Hardening plan §8. A stdlib-only, import-safe, unit-tested Python package that
ports the stabilized bash install logic into testable orchestration. Per the
plan's migration path this is ADDITIVE — install.sh stays canonical and this is
NOT wired into it yet; it is the validated foundation a future thin-bootstrap
install.sh can call.
backend/serverkit_installer/:
- _proc.py — sudo-aware subprocess helpers, mirroring app/utils/system.py
WITHOUT importing app (the bootstrap runs before the venv
exists, so it must not pull in Flask).
- distro.py — OS family mapping (a faithful port of install.sh's
os_family_from, rhel-before-fedora) + container/WSL/systemd
predicates (mirroring scripts/lib/env.sh).
- deps.py — loads scripts/deps/manifest.yaml (its primary consumer) and
resolves base/python/node/docker/pkg-manager per family.
- firewall.py — detect + pure open/close command builders (port of
scripts/lib/firewall.sh).
- service.py — init detect + pure start/stop/enable/reload builders (port of
scripts/lib/init.sh).
- main.py — composes a READ-ONLY install "plan" + `python -m
serverkit_installer plan|detect` CLI. No host mutation.
Tests: backend/tests/test_serverkit_installer.py — 46 pure-logic cases
(family mapping vs os_family_from, real-manifest resolution for all six
families, firewall/service command builders, env predicates, composed plan).
46 passed; py_compile + pyflakes clean. The CLI proves the manifest is genuinely
consumed (Debian -> apt + python3.11 + base packages), not dead config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend:
- The monitoring / metrics / telemetry(events) / uptime / fleet / status-page
read surfaces are re-mounted under /api/v1/observability/<domain> as true
aliases (same blueprints, distinct names). Original prefixes stay; the public
status page route /api/v1/status/public/<slug> is untouched.
Frontend:
- The Monitoring tab group becomes "Observability": Monitoring · Events
(the SystemEvent stream, formerly the standalone "Telemetry" page) · Status
Pages, sharing one PageTopbar. /observability lands on Monitoring.
- The standalone Telemetry route + sidebar item are folded into the group
(sidebar "Monitoring" → "Observability"; Events covered by matchPrefixes;
removed from ADVANCED_ITEM_IDS). Command palette updated.
- Fleet Monitor stays under Servers (server-scoped) and GPU stays its own
hardware page — intentional, per the plan.
Deferred (plan §4 deep merge, documented): the unified events/metrics/checks/
alerts/thresholds tables + collector migration into the job system. The
read-surface namespace + IA consolidation ship now; the schema merge is a large
data migration best done against a live metrics store.
Verified: 3 backend namespace tests + telemetry suite pass (22); frontend build
+ eslint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Additive foundation + one read namespace for the three parallel deploy records
(versioned build Deployment, webhook GitDeployment, DeploymentJob executions).
Backend:
- DeploymentJob gains deployment_id / git_deployment_id / webhook_id FKs +
commit_hash / image_tag / container_id, so it can be the canonical execution
record pointing at the release ledgers it produces (surfaced in to_dict).
Migration 039 (idempotent, additive).
- New /api/v1/deployments surface:
GET /apps/:id/history?source=build|webhook|all → federated, source-tagged
GET /:id → build Deployment detail
and the DeploymentJob surface re-mounted at /api/v1/deployments/jobs (alias of
/deployment-jobs). Per-system blueprints (/builds, /deploy, /git) unchanged.
Deliberately NOT done (plan §3 Phase 1/2 deep merge, with rationale): folding
GitDeployment into Deployment+DeploymentJob and async-ifying the currently
synchronous build/webhook deploys. Recon confirmed those paths are synchronous
with distinct failure/timeout semantics and GitDeployment carries rollback state
(snapshot_data, rolled_back_to_id); flipping them to the queue can't be verified
without a live build/runner, so it's deferred behind the additive links above.
Verified: 6 new tests + previews/config-snapshot regression pass (35).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Additive API consolidation (plan §7 Phases 3–4) that exposes a WordPress site
through its Application id, treating WordPressSite as the 1:1 extension table it
already is:
- GET /apps/:id/wordpress → {is_wordpress, site_id, env_type, ...}
- GET /apps/:id/db-snapshots → WP DB snapshots (was §8-deferred)
- POST /apps/:id/db-snapshots → create
- DELETE /apps/:id/db-snapshots/:id → delete
All grant-aware via the apps.py access seam (ResourceGrantService).
Deliberately NOT done (plan §7 Phase 1/2, with rationale): blanket-setting
app_type='wordpress' on WordPressSite-linked apps. Recon showed many WP sites are
app_type='docker', and 14+ docker-compose/nginx/scale/sleep code paths key on
app_type=='docker' — changing it would silently break start/stop/restart/logs/
status/domain config for containerized WP. The codebase already treats the
WordPressSite record (not app_type) as the source of truth (wordpress.py
_is_wp_app), which is the correct architecture; flipping app_type would regress
it. Frontend WordPress→Services merge likewise deferred (the /wordpress/:id route
is site-id-keyed).
Verified: 6 new surface tests + WP/resource-grant regression suites pass (23).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unifies "SSL Certificates" and "Advanced SSL" into one lifecycle surface.
Backend:
- The advanced cert operations (wildcard DNS-01, SAN, custom upload, TLS
profiles, health, expiry alerts) now mount under /api/v1/ssl alongside the
basic certbot routes — no path collisions. /api/v1/ssl/advanced kept as a
deprecated alias (same blueprint, second registration).
Frontend:
- api client advanced methods repointed to the unified /ssl/* paths.
- SSLCertificates.jsx (already the single SSL page) gains the two cert-creation
paths that were previously UI-inaccessible: a "Wildcard (DNS-01)" mode in the
New Certificate modal (Cloudflare token / Route 53 keys) and an "Upload"
action + modal for bring-your-own PEM cert/key/chain.
Deferred (documented follow-up): a unified Certificate registry model + disk
scan, and surfacing health/profiles panels — the API is unified and the client
methods exist; these are read-side enhancements.
Verified: 3 backend route-unification tests + existing SSL suites pass (19);
frontend build + eslint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the email unification: the outbound relay is now one smtp
EmailProviderConnection flagged uses_relay, so a single table drives both the
Notification Bus and the Postfix relay. The legacy single-row email_relay_config
is folded in on first boot and kept only as a read fallback.
- EmailRelayService rewritten over the unified table: get/save/disable/test all
operate on the uses_relay smtp connection; password stays Fernet-encrypted in
credentials_json; masked round-trips preserve the stored secret. Postfix apply
unchanged (no-op when Postfix absent).
- migrate_legacy_config(): idempotent fold of email_relay_config → connection
(password copied verbatim — same Fernet key); wired into the boot-time legacy
migration block.
- API: PUT /notifications/admin/email-providers/:id toggles usage flags
(uses_notifications/uses_relay/relay_priority); uses_relay honored for SMTP only.
Verified: 6 new relay tests + usage-flag + notification-bus suites pass (30 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation for folding the single-row email_relay_config into the multi-row
EmailProviderConnection so one table drives notifications and the Postfix relay.
- EmailProviderConnection gains uses_notifications (default true), uses_relay
(default false), relay_priority (default 0); surfaced in to_dict.
- Notification selection (default_provider) now restricted to uses_notifications
rows, so relay-only connections are never used to send notifications.
- New relay_provider() selects the active SMTP connection by relay_priority.
- add_provider() accepts the flags; uses_relay honored only for provider=smtp
(API providers can't be a Postfix smarthost).
- Migration 038 (idempotent additive columns).
Additive — existing providers default to uses_notifications=true / no relay.
Verified: 4 new tests + notification-bus suite pass (24 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the §8 API consolidation: one admin surface drives a BackupPolicy for
ANY target — application, wordpress_site, database, files, server — by
(target_type, target_id):
GET/PUT /api/v1/backups/policies/<type>/<id> (PUT accepts
target_subtype + target_meta to describe database/files targets)
POST /api/v1/backups/policies/<type>/<id>/run
GET /api/v1/backups/policies/<type>/<id>/runs
POST .../runs/<run_id>/restore | /verify
DELETE .../runs/<run_id>
This is the only way to drive the database/files/server targets (which have no
per-resource page). The grant-aware per-resource mounts
(/apps/:id/backup-policy, /wordpress/sites/:id/backup-policy) are unchanged.
Deferred to §7 (WordPress-as-app): surfacing WP DatabaseSnapshot under
/apps/:id/db-snapshots, and the frontend Backups.jsx rewrite, both of which
depend on the app↔site unification.
Verified: 5 new tests; full backup/snapshot suite green (63 passed, 2 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recorded §8 decision: a deployment config snapshot is shown as a "Config
Checkpoint" in the UI to end the overloaded "snapshot" wording. Distinct from
data-protection backups and from WordPress DB snapshots, which are untouched.
Backend:
- snapshots blueprint serves every route under /apps/:id/config-checkpoints
in addition to /apps/:id/snapshots (dual @route — both resolve to one view).
Frontend:
- DeployTab section "Deployment Timeline" → "Config Checkpoints" with reworded
hint; ConfigDiffModal + DeploymentTimeline empty/peer copy use "checkpoint".
- api/snapshots.js gains getConfigCheckpoint(s)/Diff/restore aliases.
- ProtectionStatusCard: "Snapshots run on the configured schedule" → "Backups"
(that card describes backups, not config checkpoints).
Verified: 2 alias tests + existing config-snapshot suite pass; frontend build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the cross-target queries the unified Backups page needs:
- GET /api/v1/backups/policies (filters: target_type, enabled) — replaces the
legacy filesystem schedule list with one view of every BackupPolicy.
- GET /api/v1/backups/runs (filters: target_type, status, policy_id, limit≤500)
— every BackupRun across all policies, each augmented with its target's
type/id/subtype so the UI filters without a second lookup.
Both admin-guarded like the rest of /backups.
Verified: 6 new API tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folds the legacy BackupService database/files capabilities into the unified
BackupPolicyService so one policy/run system performs every backup kind.
- _resolve_target() now handles database (connection descriptor from
target_meta + target_subtype engine), files (path list), and rejects
server (not yet implemented) with a clear message.
- _execute_backup() routes database → BackupService.backup_database and
files → BackupService.backup_files, building consistent run metadata
(kind=full, includes, primary_archive).
- Restore: _restore_database (symmetric with restore_database) and
_restore_files (safe extract into a restores/ staging dir, never blind
overwrite of a basename-only path-list archive); run_restore_job dispatches
by target_type.
Note: remote-copy upload still applies to dir-based runs (app/WP); single-file
database/files runs are local-only for now (documented follow-up).
Verified: 8 new executor tests (BackupService mocked) + B1 + existing
backup-protection suite pass (26 passed, 2 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First slice of the backups/snapshots unification. Makes the policy-driven
BackupPolicy/BackupRun system able to describe every data-protection target,
not just application/wordpress_site — the foundation for folding the legacy
BackupService (database/files) into one system.
- BackupPolicy gains target_subtype + target_meta_json (+ get/set helpers,
surfaced in to_dict). VALID_TARGET_TYPES = application, wordpress_site,
database, files, server.
- BackupPolicyService.validate_target_type(); get_or_create_policy() accepts
and (idempotently) refreshes target_subtype/target_meta for non-resource
targets like database/files.
- Migration 037 (idempotent, additive columns).
Additive only — no behavior change for existing app/WP policies.
Verified: 6 new tests + existing backup-protection suite pass (18 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feature-unification plan §2. Ends the naming collision between generic
/projects (workspace organization) and WordPress "Projects" (environment
pipelines). Label + URL change only; deeper WP/app unification is §7.
Frontend:
- Pipeline list moves to /wordpress/pipelines; detail to
/wordpress/pipelines/:id[/:tab]. WORDPRESS_TABS, command palette, page
titles, breadcrumbs, empty-state copy, and the WordPressProject tab base
all updated to "Pipelines".
- Legacy /wordpress/projects[...] URLs redirect to the pipelines space via
LegacyWpPipelineRedirect (deep links preserved).
- wordpressApi.getPipelines() added; getProjects() kept as back-compat alias.
Backend:
- environment_pipeline_bp mounted a second time under
/api/v1/wordpress/pipelines as a true alias of /wordpress/projects.
- Test: /pipelines resolves identically to /projects.
Verified: frontend build clean, 5 alias tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feature-unification plan §1 (Services UI = Applications backend). Ends the
"Applications vs Services" terminology split by collapsing the two detail
pages into one canonical surface and aliasing the backend.
Frontend:
- Merge ApplicationDetail's exclusive tabs (Container Ops, WAF, Build, Deploy,
Previews) into the canonical ServiceDetail (PageTopbar chrome). Type-gating
preserved exactly: ops=Docker, waf=Docker+Python, build/deploy/previews=all.
- serviceTypes.js tab lists extended for the merged tabs.
- /apps/:id and /apps/:id/:tab now redirect to /services/:id[/:tab] via
LegacyAppRedirect (deep links preserved); ApplicationDetail.jsx deleted.
- Drop the /apps page-title entry and "Application Details" dynamic title.
- api.getServices()/getService() aliases over getApps()/getApp().
Backend:
- Mount apps_bp a second time under /api/v1/services as a true alias of
/api/v1/apps (Flask blueprint re-registration with name='services').
- Test: /api/v1/services resolves identically to /api/v1/apps.
Verified: frontend build clean, eslint 0 errors, 3 alias tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Block stopping/restarting/removing ServerKit's own Docker containers and surface that in the UI. Backend: add DockerService helpers (is_protected_name / is_protected_container), route guard (_reject_if_protected) to reject destructive actions, annotate list_containers with a protected flag, and update cleanup logic. Add tests (backend/tests/test_docker_protection.py). Frontend: hide/replace lifecycle controls for protected containers (lock badge + styles), add Organization tab group (ORG_TABS) and migrate Projects/Shared Variables/Workspaces into a TabGroupLayout with topbar action hooks; update related styles and sidebar item matching.
A variable can now land on one compose service instead of all of them. Both
EnvironmentVariable and SharedVariable gain an optional target_service
(NULL = all services), threaded through the shared resolver as an inclusion
filter (a var applies to a service when it targets all OR that service).
EnvService.get_effective_env_for_services(app, services) computes each service's
merged env (all-services vars + that-service vars, local overriding shared), and
ComposeEnvService builds the override's per-service environment from it — so a
var targeted at "db" only appears under the db service, untargeted vars appear
everywhere. The single-container docker-run path is unchanged (one container
gets the full effective env regardless of target).
API: env-var create/update and shared-variable create/update accept
target_service; GET /apps/<id>/compose-services lists a compose app's services.
UI: the Environment tab shows an "Applies to" selector (gated to compose apps via
the service list) with a per-row target chip; the shared variable editor gains a
target-service field.
Migration 036 adds the two columns; single Alembic head; chain applies from
empty. Proving tests cover per-service distribution + the resolver helper. Full
backend suite: 949 passed, 14 skipped; frontend builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single-container apps already get the effective env via docker run -e. Compose
apps get container env from the compose file's environment:, so this wires them
the same way through a managed override compose file.
ComposeEnvService.refresh_for_project writes docker-compose.serverkit.yml next to
a managed app's base compose, adding an environment: overlay to every service
built from EnvService.get_effective_env (shared variable groups under the app's
own local env vars). Compose merges the override on top of the base, so the
effective env wins on collisions — consistent with the docker-run path. Dollar
signs are escaped ($ -> $$) so values survive compose interpolation intact.
DockerService.compose_up now includes the override as a second -f (via
_compose_cmd_with_overlay); `up -d` recreates changed containers so updated env
takes effect on the next deploy. It's non-destructive (ServerKit only owns the
override; the base compose is never edited), best-effort/guarded (a build failure
falls back to the plain base command and never blocks a deploy), regenerated each
up, and removed when the app has no effective env. Non-app dirs (e.g. proxy
stacks) and env-less apps are left untouched.
Proving test test_compose_env_overlay.py (6 cases): overlay injects shared+local
into every service with local winning collisions, $ escaping, stale-override
cleanup, non-app dirs untouched, and compose_up running with -f base -f override.
Full backend suite: 947 passed, 14 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shared variable groups were resolved/displayed but never reached the container.
EnvService.get_effective_env(app) now merges them in: shared groups
(workspace < project < environment < direct) form the base layer and the app's
own local env vars override them, so a key set in both yields the local value —
matching the "Set locally — local value applies" UI hint. Secrets are decrypted
for injection; shared resolution is best-effort and never blocks a deploy.
The docker deploy path (_deploy_docker) now injects this merged env via
run_container, which also fixes a latent bug there (it called .get('success')
on get_env_vars()'s list result, so env was effectively never applied). Updated
the shared-variables panel note to say shared vars now apply at deploy with
local taking precedence.
Proving test test_effective_env.py (6 cases) covers the merge precedence, the
best-effort fallback, and that _deploy_docker hands run_container the merged env.
Full backend suite: 941 passed, 14 skipped; frontend builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tightens the seams the recent features introduced:
- Secret precedence/conflict: the embedded shared-variables panel now resolves
via the hierarchical endpoint, shows a per-variable source-scope badge +
precedence legend, and flags keys that are also set locally with a
"Set locally — local value applies" badge (shared groups are a facade and are
not injected into the container; the app's own env var is authoritative).
- Split inventory: apps now expose project_name/environment_name; the Services
list shows a project/environment badge (or "Unassigned") and a "Move to
project" bulk action (POST /apps/move-to-project, workspace-validated,
null-unassigns).
- Fleet proxy actionability: fleet_overview rows carry a {level,text}
recommendation (mismatch warning / idle stack / empty host / aligned),
rendered as a per-row pill.
- Onboarding wizard: "Step X of 5" + progress bar + rough ETA, and a
collapsible step-history trail so returning users get a clean summary.
- Config diff: a plain-language summary ("3 environment variables and the image
tag (app:1.2.1 -> app:1.1.9) changed") above the technical diff and echoed in
the restore confirmation.
- Container status: an always-visible "N issues" / "healthy/total" badge so a
degraded sub-container isn't hidden behind hover.
- Sidebar: Projects, Shared Variables and Workspaces collapse under a single
"Organization" group.
No schema change (project/environment names are derived). Backend suite: 935
passed, 14 skipped; frontend builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both reverse proxies bind 80/443, so pointing the same domain at host Nginx and
a Dockerized proxy stack is an operational footgun. This tags each app with the
plane it expects and surfaces disagreements instead of leaving them implicit.
- Each app carries an `ingress_plane` ('nginx' | 'proxy_stack'); NULL reads as
the host-Nginx default. New pure helpers in app/utils/ingress.py: only
container-based services are proxy-stack eligible, and a requested proxy_stack
is forced back to nginx for PHP/WordPress/static/Python apps. to_dict() also
exposes `ingress_proxy_eligible`.
- ProxyStackService.ingress_audit(server) reports which of a server's apps
disagree with its active proxy mode; fleet_overview now includes per-server
app_count + mismatch_count. New GET /servers/<id>/proxy/ingress-audit.
- Create wizard offers the proxy stack only for Docker/Compose services and
keeps the default on Nginx; app detail shows an Ingress badge; the fleet proxy
dashboard flags mismatch counts; the server proxy panel warns and lists the
specific mismatched apps with reasons.
Migration 035 adds applications.ingress_plane. Backend suite green (incl. 8 new
ingress tests); frontend builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Server onboarding completion (C8): a pure pre-flight compatibility check
(arch/distro/kernel/cpu/memory) gating validate; real-but-guarded
distro-aware install_prerequisites/install_docker/pair_agent steps that
no-op safely on dev/Windows and never raise; audit + best-effort
notifications on each transition; and a live auto-scrolling log trail +
elapsed timer in the onboarding wizard.
- Fleet-wide proxy dashboard (C6): ProxyStackService.fleet_overview aggregates
every server's proxy posture (configured stack vs host Nginx default) behind
GET /api/v1/servers/proxy/overview, surfaced as a new Fleet Proxy page in the
Servers tab group with summary tiles + click-through to each server's proxy.
- Polymorphic shared resources depth (C9): hierarchical variable resolution
(workspace < project < environment < direct attachment < resource) with
source provenance and {{group.KEY}} interpolation, an audit trail on every
tag/group/variable/attachment mutation, GET /api/v1/shared/resolved/
hierarchical, and the reusable Tags/Environment-Variables panels embedded on
the application and server detail pages.
- Docs & coverage: docs/ENHANCEMENTS.md (brand-neutral guide to all ten
capabilities + verified endpoints), README/CHANGELOG updates, and a
cross-cutting integration test asserting the new endpoints are wired.
Full backend suite: 924 passed, 14 skipped; frontend builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Project / Environment hierarchy (C2): Workspace -> Project -> Environment
org model. New Project/Environment models + CRUD APIs, nullable
applications.project_id/environment_id (opt-in, no destructive backfill),
ProjectService.ensure_default, a Projects list + ProjectDetail (env tabs),
a project/environment selector in the create wizard, and sidebar nav.
- Polymorphic shared resources facade (C9): ResourceTag + SharedVariableGroup
/SharedVariable/attachment models keyed by (resource_type, resource_id),
reusing the env-var Fernet encryption; SharedResourceService resolves merged
variables (most-recent attachment wins) with secrets masked. /api/v1/shared
endpoints + reusable TagsPanel/EnvironmentVariablesPanel + a Shared Variables
management page. Existing per-resource tables untouched (facade first).
- PR preview environments (C3): ApplicationPreview + settings models, a
PreviewService with pure domain-templating + reconcile (open PRs vs active
previews) cores and best-effort provisioning (incl. guarded WordPress
file+DB clone), a pull_request webhook endpoint, preview.* jobs, and a
Previews tab on the app detail page.
- Per-server managed proxy stack (C6): opt-in Dockerized Traefik/Caddy as a
Compose stack (host Nginx stays default and superior for PHP/WordPress).
ProxyStack model + ProxyStackService with pure compose/config generators and
versioned config backups, /api/v1/servers/<id>/proxy endpoints, and a Proxy
tab on the server detail page.
Migrations 031-034 add the new tables/columns; the full chain applies cleanly
from an empty DB. Full backend suite: 871 passed, 14 skipped; frontend builds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Build packs / zero-Dockerfile deploys (C1): new BuildpackService detects the
stack from a cloned repo (Node/Python/Go/PHP/Ruby/Rust/static), produces a
transparent build plan and a generated Dockerfile, and persists the plan +
overrides on the Application. POST /api/v1/buildpacks/{detect,generate}; a
BuildpackPreview shows the detected plan + editable overrides + Dockerfile in
the create wizard and the app Build tab. Builds on the existing build_method
plumbing instead of duplicating it.
- Deployment config snapshots + diff engine (C4): ConfigurationService captures
an immutable, hashed snapshot of resolved config (env keys + masked values,
domains, image/tag, build method, volumes) at every deploy and rollback; new
DeploymentSnapshot model + /api/v1/apps/<id>/snapshots[/diff|/restore]. A
DeploymentTimeline + ConfigDiffModal surface "what changed" and one-click
restore in the app Deploy tab. Secret values are never serialized.
- Template catalog certification (C5 cont.): a whole-catalog validation test
proves all 105 declarative templates conform to the documented schema; fixed
the stale index.json (was 63/105 entries) to list every bundled template.
Migration 030 adds the buildpack columns + deployment_snapshots table. Full
backend suite: 820 passed, 14 skipped; frontend builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Four foundational enhancements toward the deploy-code developer experience:
- Container status aggregator (C7): new ContainerStatusService applies a
deterministic priority hierarchy (degraded > restarting > running:unhealthy >
starting > running:healthy > exited > unknown) over an app's containers,
cached with a short TTL. Exposed at /api/v1/status and pushed over a new
container_status Socket.IO channel; Applications page now renders an
aggregated ContainerStatusPill with sub-status tooltip.
- API token scopes + sensitive data masking (C10): require_scope() decorator
enforces fine-grained scopes for API-key requests (pass-through for JWT),
a canonical SCOPES catalog at /api/v1/api-keys/scopes, and a reusable
sensitive_data_filter (mask_sensitive) honoring secrets:read. Frontend gains
a grouped ApiKeyScopesModal picker and per-key scope pills.
- Server onboarding state machine (C8): new onboarding lifecycle
(pending -> validating -> installing_prerequisites -> installing_docker ->
pairing_agent -> ready/failed) with ServerOnboardingService, a
server_onboarding_logs table, servers.onboarding_* columns, REST endpoints,
and an OnboardingWizard surfaced on the server detail overview.
- Declarative template catalog (C5): documented catalog schema and
magic-variable resolution (SERVICE_PASSWORD_/USER_/FQDN_/URL_/BASE64_*)
layered onto the existing YAML template loader without changing behavior for
templates that don't use them; GET /api/v1/templates/catalog/schema.
Migration 029 adds the onboarding table + columns. 60 new tests pass; frontend
builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a ServerKit-managed per-site Fail2ban layer to protect WordPress login endpoints. Adds Fail2banJailService (write-side) to generate filter and per-site jail configs, enable/disable jails, reload fail2ban, and unban IPs (Linux-only, best-effort, ownership-guarded). Wire it into WordPress service lifecycle (enable on site create, remove on delete) and WpSecurityService wrapper methods. Expose API endpoints for status, toggle and unban in backend API. Add unit tests for Fail2banJailService (config rendering, naming/safety, lifecycle, graceful degradation). Frontend: Security tab loads brute-force status, can toggle protection, shows ban counts and unban action; API client and styles updated. NginxService gains canonical per-site log/error path helpers. Docs updated to reflect shipped brute-force jail feature.
Validates all 105 bundled templates parse + validate via the real TemplateService, ids match filenames, and every template has an icon; the 42 roadmap templates additionally use inline base64 icons, list-shaped vars, and are brand-neutral; and build_repo_index() covers the whole catalog. 149 cases, all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes the expanded catalog usable end-to-end:
- #24 Auto-domain: templates can opt in with 'auto_domain: true'; on install the app is published at <slug>.<base_domain> via the existing SiteDomainService (HTTPS only if the wildcard cert is already set up — never forces SSL; remote installs skipped).
- #25 Category consolidation: getCategoryIcon now covers the new tags (ai/llm/search/finance/documents/rss/vpn/dns/identity/observability/...).
- #26 Icon strategy: each template ships an inline lucide-style base64 icon with a keyed lucide fallback map; documented in Templates.jsx.
- #27 Update flow: update_app now normalizes list-format variables and re-renders the files: section + bind mounts, so converted/file-based templates (litellm, signoz, posthog) update correctly.
- #28 Community repo: TemplateService.build_repo_index()/export_repo_index() generate the index.json the sync mechanism already expects, exposed via GET /repos/index and POST /repos/index/export.
Roadmap: SERVICES_ROADMAP #24-#28.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the AI/LLM one-click templates and wires the panel's own AI assistant to a self-hosted Prompture Hub:
- Templates: prompture-hub, ollama-webui, qdrant, chroma, litellm, flowise, langflow, anythingllm, librechat (brand-neutral, auto-port, generated secrets, lucide icons).
- AI provider: new 'prompture-hub' provider routed through Prompture's OpenAI-compatible cachibot driver (hub key + endpoint), so ServerKit AI can call a local hub instead of raw provider keys.
- Featured Prompture Hub + icon fallbacks in the Services UI.
Roadmap: SERVICES_ROADMAP #1-#8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /domains is one table: app-linked domains + live zones from connected DNS
providers (Cloudflare, ...) with provider badges. New GET /dns/portfolio
(aggregates provider zones, marks which are adopted) and idempotent
POST /dns/adopt (lazy-materialize a managed zone on demand).
- Every row opens one drawer = the unified DNS records surface
(components/domains/DomainDnsPanel): live Cloudflare records via
GET /dns/provider-records (no adopt needed), type badges, proxied=>HTTPS hint,
inline Add record, deep-links to Cloudflare ops / full DNS page. Fixed-layout
table (no horizontal scroll); actions moved into the drawer header.
- Registration expiry/registrar from Cloudflare Registrar + lazy RDAP fallback
(GET /dns/registration), persisted/cached in the new domain_registrations
table and merged into the portfolio so it survives a refresh. New
frontend/src/utils/expiry.js formatExpiry() date util.
- Connections moved to admin-only (Settings Admin group + isAdmin gate;
@admin_required on /api/v1/connections); CF connection card links to Domains.
- Cloudflare client: expose each zone's account_id; add list_registrar_domains.
- Tests: backend/tests/test_dns_portfolio.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_canonical_site_url feeds clone/URL-swap search-replace, so it must
return the URL actually baked into the WordPress DB. A domain-less site
is installed at localhost:<port>, not the panel apex; the panel_origin
branch (added in 652d903a) made it resolve to the base domain, so
clones/swaps would search-replace nothing and silently leave stale URLs.
Drop that branch, keep the ssl_enabled-aware scheme. Fixes the
test_canonical_site_url_prefers_primary_domain regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Storage tab listing and managing Cloudflare's account-scoped storage
primitives — R2 buckets, KV namespaces, and D1 databases. Management only
(list/create/delete); the object/key/row data planes are out of scope.
- CloudflareClient: r2/buckets, storage/kv/namespaces, d1/database list+create
+delete.
- CloudflareService: combined list_storage (each product fetched independently so
a missing token scope degrades to a per-product error, not a dead tab),
validated create/delete for each. R2 bucket names validated S3-style.
- /api/v1/cloudflare/zones/:id/storage* routes (read any user; writes admin).
- StoragePanel (three generic create/list/delete sections; R2 notes its
S3-compatibility for future backup targeting); api methods; SCSS.
No new tables — wiring an R2 bucket as an S3 backup backend (minting scoped R2
keys) is a deliberate follow-up, noted in the UI.
Tests: aggregate listing + per-product error degradation, R2 name validation,
create/delete, KV/D1 required-field guards (6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Tunnels tab for creating and managing Cloudflare Tunnels (cloudflared) —
exposing a local/private service through Cloudflare's edge with no public IP.
Clearly distinct from ServerKit's WireGuard remote-access tunnels (always
labelled "Cloudflare Tunnel" in the UI). Account-scoped; account resolved from
the zone.
- CloudflareTunnel model: records the connector token (encrypted at rest) and
owning connection per (account, tunnel_id).
- CloudflareClient: cfd_tunnel create (config_src=cloudflare) / list
(is_deleted=false) / delete / token / connections / configurations get+put.
- CloudflareService: create (returns + stores the one-time token, yields the
cloudflared install command), list (managed-flagged), delete, fetch install,
and public-hostname routing — sets the ingress config (always re-appends the
required http_status:404 catch-all) and best-effort upserts the proxied CNAME
hostname→<id>.cfargotunnel.com via the shared ownership-guarded DNS path.
- /api/v1/cloudflare/zones/:id/tunnels* routes (read any user; writes admin).
- TunnelsPanel (create form + one-time install box w/ copy, tunnel list with
per-tunnel install reveal and public-hostname add/remove); api methods; SCSS.
Tests: list is_deleted param, create config_src, name required, token stored
encrypted + install string, managed-flagging, delete prunes local row, ingress
built with catch-all last (7).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Workers tab to the Cloudflare Zone Settings page for deploying edge
scripts and managing Worker routes. Workers are account-scoped, so the owning
account is resolved from the zone — the whole feature reuses the existing
Cloudflare DNS connection (no new credential).
- CloudflareWorker model: records the source ServerKit uploaded per (account,
name) so a worker can be tracked and re-deployed (created via create_all).
- CloudflareClient: zone→account lookup, list/delete scripts, and a stable
multipart PUT /scripts/{name} module upload (the still-beta resource-oriented
Workers API is deliberately avoided per the roadmap's fallback note), plus
zone Worker routes list/add/delete.
- CloudflareService: list (live scripts flagged when ServerKit-managed + routes),
deploy (validated lowercase name, records source, optional route attach),
delete, route add/delete.
- /api/v1/cloudflare/zones/:id/workers* routes (read any user; writes admin).
- WorkersPanel component (deploy form with a starter script, worker list, routes
list); api methods; SCSS.
Tests: multipart upload shape, name validation, deploy records source, list
managed-flagging, delete prunes the local row (5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a WAF tab to the Cloudflare Zone Settings page for managing custom firewall
rules (the http_request_firewall_custom phase entry-point ruleset).
- CloudflareClient: ruleset list/get + phase-entrypoint create (PUT) + per-rule
POST/PATCH/DELETE.
- CloudflareService: list rules (empty when no custom ruleset yet, distinguished
from an auth error by listing rulesets first), add rule (creates the ruleset on
first use), update/delete, and three one-click presets — lock WordPress admin
to an IP, block common exploit paths, challenge suspicious bots. The IP in the
lock preset is validated with ipaddress so it can't inject expression syntax;
rule actions are constrained to a safe terminal subset.
- /api/v1/cloudflare/zones/:id/waf/* routes (list any user; add/preset/update/
delete admin).
- WafPanel component (preset picker + custom-rule form + active-rules list with
enable toggle and delete); api methods; SCSS.
Tests: ruleset discovery, empty vs auth-error, create-on-first-rule vs append,
action validation, preset IP validation + safe expression, update/delete (10).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Cloudflare cache management to the Zone Settings page. Purge everything (with
a confirm) or purge specific URLs (one per line, capped at 30 to match Free/Pro
limits); hosts/prefixes/tags are accepted by the API for Enterprise plans and
their plan errors are surfaced verbatim.
- CloudflareClient.purge_cache(zone_id, payload).
- CloudflareService.purge_cache: validates intent (raises on empty), caps files
at 30, normalizes the payload.
- POST /api/v1/cloudflare/zones/:id/purge-cache (admin).
- Actions tab gains a "Purge cache" card (purge-everything button + URL list);
api method; SCSS.
Tests: client payload, purge-everything, 30-file cap, empty-raises, provider
error surfaced (5 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Cloudflare Zone Settings surface on top of the existing Cloudflare DNS
connection — SSL/TLS, Speed, Caching and Security toggles plus a one-click
"recommended hardening" preset. Reuses the connected DNSProviderConfig
credential and shared CloudflareClient, so auth/encryption-at-rest are not
re-implemented.
Backend:
- CloudflareClient: generic v4 request() helper + zone-settings get/update
(envelope normalized to {success, error?, result?}).
- CloudflareService: resolve zone+credential by ServerKit DNS zone id, curated
setting groups (UI metadata), get/update setting, apply recommended preset
with per-setting reporting.
- /api/v1/cloudflare blueprint: settings GET / GET-one / PATCH + apply-preset
(reads any user, writes admin; mutations captured by the audit fallback).
Frontend:
- CloudflareZoneSettings page (tabbed SSL/TLS · Speed · Caching · Security ·
Actions) with declarative toggle/select/HSTS controls and optimistic save.
- Route /cloudflare/zones/:zoneId, "Cloudflare" link from Cloudflare-managed
DNS zones, api module, SCSS.
Tests: client envelope/zone-settings + service resolution guards, indexing,
and preset reporting (11 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#12 SiteDomainService.give_subdomain(app, label): one-click publish an app at
<slug>.<base> — primary Domain row + nginx vhost + (per-site mode) an auto-created,
guarded/logged A record. Endpoints: POST /domains/give-subdomain and
GET /domains/suggest-subdomain.
#14 GET /api/v1/dns/managed: every record ServerKit owns across all provider zones,
enriched with the app that triggered each (from the ownership ledger).
#13 inline propagation reuses the existing /dns/propagation/<domain> check.
Tests: test_dns_give_subdomain.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#9 sites_dns_mode setting (wildcard | per-site) via SiteDomainService.dns_mode();
surfaced in SitesHttpsService.status().
#10 One-click wildcard already exists (SitesHttpsService.setup creates *.<base> +
issues the wildcard cert); it now also pins the mode to wildcard, and the wildcard
A record flows through the Phase 1 guarded/logged path.
#11 SiteDomainService.ensure_site_dns(): in per-site mode a new managed site gets
its own A record auto-created via a connected provider (ownership-guarded + logged),
wired into WordPressService._provision_routing. Wildcard mode stays a no-op.
Tests: test_dns_subdomain_modes.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#6 dns_changes audit log (migration 027) — every record write ServerKit sends to a
connected provider is recorded at the write choke point (guarded_upsert /
guarded_delete): create/update/delete, source, result (ok|error|conflict|skipped),
and the value set. Powers the "Changes to your Cloudflare" activity feed.
GET /api/v1/dns/changes lists it (filter by config_id / zone / result).
#8 A real write failure now surfaces an admin notice via the Notification Bus
(dns.sync_failed) instead of being swallowed to the log. Best-effort — surfacing
never breaks the write it describes.
Tests: test_dns_change_log.py. The frontend activity/mirror UI lands separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#3 managed_dns_records ledger (migration 026) — the single source of truth for
records ServerKit created in a connected provider zone, written by every write
path (zones page, DDNS, WordPress auto-DNS, email).
#4 DnsOwnershipService.guarded_upsert/guarded_delete: automatic paths refuse to
overwrite or delete a record ServerKit didn't create (the user's own records),
while the explicit Zones page adopts a matching record and takes ownership.
Wired into both CF paths (DNSZoneService._cloudflare_sync and
DNSProviderService._cloudflare_set_record/_delete_record). ensure_a_record now
surfaces a 'foreign_record' reason instead of clobbering.
#5 Live zone mirror: CloudflareClient.list_records +
DNSZoneService.list_provider_records classify every live record as serverkit vs
external; GET /api/v1/dns/<id>/mirror.
Tests: test_dns_ownership.py. Existing set_record mocks updated for the new
source kwarg.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 0 of the DNS roadmap.
#2 Shared Cloudflare client (app/services/dns/): DnsCredential + DnsRecordSpec +
CloudflareClient. Both DNSProviderService and DNSZoneService delegate to it, so
auth (scoped token / global key), the CAA structured-data wire format, MX/SRV
priority parsing, and idempotent upsert live in one place. Fixes dup-on-recreate,
silent update/delete no-op, and missing proxied/priority on the provider path.
#1 Single credential source: new FK dns_zones.dns_provider_config_id (migration
025) + DNSZoneService._resolve_credential (linked -> auto-discover-by-domain ->
legacy inline-token). create_zone accepts a connection id; link_legacy_zones()
converges inline tokens onto encrypted connections at boot. The /dns create-zone
modal now picks a connection instead of pasting a token. DDNS rides it for free.
Tests: test_dns_cloudflare_client.py, test_dns_credential_resolution.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add GET /api/v1/system/notices endpoint (admin-only)
- Detect missing canonical domain, sites base domain, server public IP, and wildcard HTTPS
- Add SystemNotices React component with dismiss persistence in localStorage
- Render notices inside DashboardLayout so they appear on every page
- Add scoped SCSS for warning/info/error notice banners
Make email handling case-insensitive (queries + model normalization) to prevent duplicate accounts. Add SiteDomainService.panel_origin() helper and use it across services to build public URLs (Gitea, WordPress, Roundcube) and Django ALLOWED_HOSTS. Add optional custom-domain support when creating WordPress sites and propagate domain to Roundcube install. Improve environment domain fallback to prefer panel/base domains over .localhost. Small Dockerfile healthcheck tweak (use 127.0.0.1). Update install/update/build scripts to always (re)create virtualenv locally for release installs. Remove unused frontend CreateSiteModal and expose domain field in WordPress page.
Telemetry: ensure sanitized payloads bigger than the 32KiB threshold are replaced with a {_truncated: True} marker before storage, and add a unit test covering that behavior.
Backend API: adjust imports in secrets_webhooks to use WorkspaceService and include User model.
Frontend: make Sidebar resilient to malformed active_workspace JSON in localStorage (try/catch), and ensure socketService.disconnect() is called when user becomes unauthenticated in NotificationsProvider to clear notifications and unread count.
Files: backend/services/telemetry_service.py, backend/tests/test_telemetry.py, backend/app/api/secrets_webhooks.py, frontend/src/components/Sidebar.jsx, frontend/src/contexts/NotificationsContext.jsx