Commit Graph
289 Commits
Author SHA1 Message Date
Juan Denis b7911026be Merge plan-47 lean-core (wizard installs + 4 backend extractions + security tab-group) 2026-07-15 15:24:07 -04:00
Juan DenisandClaude Opus 4.8 3954680c87 fix: apply login layout to the main login form + bind BYO-image legacy port to loopback
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>
2026-07-15 13:55:15 -04:00
Juan DenisandClaude Opus 4.8 882b7bb8e2 feat: brand-neutral public title + per-install favicon tint
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>
2026-07-15 13:12:23 -04:00
Juan DenisandClaude Opus 4.8 9a1a1f69a3 feat: configurable panel title + switchable login layouts
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>
2026-07-15 13:01:39 -04:00
Juan DenisandClaude Opus 4.8 1428aa8f29 feat(apps): start local image apps that have no source on disk
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>
2026-07-15 11:50:55 -04:00
Juan DenisandClaude Opus 4.8 95681f8b80 refactor(status): extract status-pages backend into serverkit-status extension
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>
2026-07-14 22:34:24 -04:00
Juan DenisandClaude Opus 4.8 e6bf4915b4 refactor(remote-access): extract WireGuard-tunnels backend into extension
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>
2026-07-14 22:27:02 -04:00
Juan DenisandClaude Opus 4.8 44a73d7796 refactor(cloud): extract cloud-provision backend + add upgrade re-acquisition
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>
2026-07-14 22:22:18 -04:00
Juan DenisandClaude Opus 4.8 2a060c518d refactor(ftp): extract FTP backend into serverkit-ftp builtin extension
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>
2026-07-14 22:15:07 -04:00
Juan DenisandClaude Opus 4.8 f58bdb6823 feat(setup): wizard installs recommended extensions; lean-by-default flagships
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>
2026-07-14 22:11:59 -04:00
Juan DenisandClaude Opus 4.8 531fdf3d42 refactor(automations): retire Workflow Builder engine, port emitters (Phase 4 backend)
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>
2026-07-11 22:15:10 -04:00
Juan DenisandClaude Opus 4.8 2dfe0d7d54 feat(search): unified entity omnisearch endpoint for the palette
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>
2026-07-11 00:26:40 -04:00
Juan DenisandClaude Opus 4.8 5fc0b881b4 feat(connections): one-click GitHub setup via the GitHub App manifest flow
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>
2026-07-10 23:06:09 -04:00
Juan DenisandClaude Opus 4.8 abe0fbece9 feat(templates): deploy templates as a backend repo-kind catalog entry
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>
2026-07-10 02:38:46 -04:00
Juan DenisandClaude Opus 4.8 4833f91451 feat(notifications): restore email-bounce webhook slice (plan 42 ph3)
Restore the plan-33 bounce slice lost in the rebuild (migration 069 survived;
the model/route/service did not). EmailBounceState model reconstructed verbatim
from the clean pyc dump. New bounce_service.BounceService maps provider payloads
(generic/SendGrid/Postmark/SES/Mailgun), records bounces correlated to
NotificationDelivery by provider_message_id, auto-mutes a recipient past
MUTE_THRESHOLD (complaints mute immediately), and the email channel skips muted
targets. Inbound webhook POST /api/v1/notifications/inbound/email (HMAC-SHA256,
404 until notify.inbound_secret set, 401 on bad sig) + operator unmute + admin
bouncing list + email_status in GET /preferences.

Route/service names/paths faithful to the salvaged test string-index; logic
reconstructed from the plan-33 doc (the route+test pycs were corrupt). Model
registration in app/models/__init__.py lands with the Phase-2 commit (shared
file). Verified: test_email_bounce.py 16 passed; notification suite no regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:55:53 -04:00
Juan DenisandClaude Opus 4.8 ba271a6850 fix(dns-cutover): restore snapshot() staging + provider-named 501 (plan 42 D7/ph5)
The recovery rebuild left the provider-registry cutover path a subset: the
explicit-records staging classmethod DnsCutoverService.snapshot(domain, records,
provider=, provider_zone_id=) was gone (only the server-sourced create_snapshot,
which needs a live client, survived), and the 501 NO_PROVIDER body didn't name
the provider. Re-add snapshot() (sibling of create_snapshot; stages caller-
supplied records with no client so the provider guard fires at cutover time) and
thread DnsCutoverError.provider through _resolve_dns_client into the API bodies.

Un-skips test_dns_cutover_providers (plan 31 #12). Kept the rebuilt contract's
UNWRAPPED /cutover success shape (proven by the surviving test_dns_cutover_api.py)
rather than the pre-loss 'cutover'-wrapped shape, adapting that one assertion.
23 passed (providers + api + service), no regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:50:16 -04:00
Juan DenisandClaude Opus 4.8 be3a9f9bcd feat(manifest): finish appliance volumes + one-shot bootstrap (plan 35 ph2)
Persist manifest disk `size` onto AppVolume.declared_size; route disk backups to
the docker volume's live host mountpoint (VolumeService.host_path_for_mount +
volume_mount meta in BackupPolicyService._resolve_target). Add `bootstrap`
schema + BootstrapService.run_once (docker compose run --rm, injectable runner
seam), a bootstrap apply step that stamps applications.bootstrap_done once, and
POST /manifests/bootstrap/reset to re-arm it. Scaffold round-trips disk size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 03:05:08 -04:00
Juan DenisandClaude Opus 4.8 06c3fcf28c feat(manifest): appliance-tier typed L4 ports + plan-time blockers rail (plan 35 ph1)
Rebuild of plan 35 Phase 1 against the surviving migrations (ports as a JSON
column on applications, not a separate table). Adds typed `ports[]` to the
serverkit.yaml schema/normalizer, AppPortService (persist/compose-emit/firewall),
open_port apply step + firewall open on apply / close on app delete, and the
blockers engine (port_conflict/remote_target/observed_server/firewall_undetected)
that refuses apply instead of half-deploying.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 02:57:23 -04:00
Juan DenisandClaude Opus 4.8 bd6b8eff33 feat(plugins): GitHub-native install with preview + permissions consent
Add POST /api/v1/plugins/preview: resolve a GitHub/zip source, download,
read plugin.json, and return slug/version/permissions/panel-gate + the
resolved URL, sha256, and warnings — without installing. The manual-install
URL flow becomes two-step (Resolve -> consent card -> Install), pinning the
install to the previewed sha256 so installed bytes match previewed bytes.

- owner/repo and owner/repo@tag shorthand normalization (install accepts it too).
- Actionable resolve errors: rate limit (with reset ETA), 404/private, no
  plugin.json at archive root.
- Optional SERVERKIT_GITHUB_TOKEN: Bearer auth on GitHub hosts only (never
  leaked to third-party mirrors, never logged); lifts the anon rate limit and
  enables private-repo installs.
- ManualInstallModal: PreviewConsent card with permission chips + warnings +
  'GitHub · unverified' badge; SCSS added.

Tests (test_plugin_preview.py, 15): shorthand, happy/warning preview paths,
token header injection + host scoping, preview->install checksum continuity +
tamper rejection. Full plugin suites green; frontend lint 0 errors, build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:52:38 -04:00
Juan DenisandClaude Opus 4.8 c7b12b3095 feat(marketplace): surface registry v2 logo/repo/bundled fields
Stop stripping the v2 fields in registry_service normalization: add repo,
logo, bundled to _FIELDS. Repo-relative logos (assets/<slug>/<file>) are
resolved to absolute URLs against the index URL they came from, so one
relative path works for both the raw-GitHub index and the serverkit.ai
/ext endpoint. Bundled entries are excluded from the Browse catalog by
default (they duplicate builtin cards) and reachable via
?include_bundled=true. Detail modal gains a Source repo link.

Tests: v2 normalization, relative-logo resolution, v1 unaffected, bundled
exclusion + include_bundled endpoint flag. Full test_registry.py green (19).
Docs: EXTENSIONS_REGISTRY.md documents schema_version 2 + v2 fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:43:34 -04:00
Juan DenisandClaude Opus 4.8 634f21340d feat(jobs): host-idiom Jobs page + server-side search/paging + job retention
Frontend: rebuild /jobs to the Servers/Domains table-with-search idiom —
SegControl status filter, kind Select, debounced search, DataTable rows,
clickable compact KPIs, Prev/Next paging with 'Showing X-Y of N'. Rewrite
_jobs.scss for the sk-jobs namespace (dead .jobs-* selectors removed).

Backend: GET /api/v1/jobs gains a case-insensitive `q` (kind/owner) filter
and a `total` count; JobService.prune_terminal + a builtin.job_retention
ScheduledJob prune scheduler-tick jobs (succeeded/cancelled > jobs.retention_days
default 14, failed > 3x) so the '107k Total' stops growing at the source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 23:01:12 -04:00
Juan DenisandClaude Opus 4.8 ea07a1ec14 fix(recovery): restore agent scopes, DNS preflight, catalog events, thread guards
Assorted lost-code fixes surfaced by surviving tests.

- server.py: restore 'probe'/'survey' READ_ACTION_VERBS + is_read_action (agent
  scope classification, plan 28); servers.py scoped profiles grant doctor/
  systemd/survey read scopes
- site_domain_service: restore _host_resolves (bounded DNS check) + creation_
  warnings/_GAP_FIX_LINKS so setup preflight flags host_unresolved (plan 22/31)
- notifications/catalog: register backup drill/verify restore-proof events
- metrics_history + api_analytics: guard the eager background-thread start under
  the testing config (no threads leak per create_app)
- remove a stale partial app/plugins/serverkit-email/ package that shadowed the
  runtime-installed email extension (broke route registration)

test_server_permissions 5, test_setup_preflight 7, test_backup_alerts 6,
test_no_background_threads 1, test_email_extraction 3 — green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 02:06:01 -04:00
Juan DenisandClaude Opus 4.8 d9b04077d4 fix(recovery): restore per-app route authz gating (plan 29)
Many per-app API route files lost their plan-29 gating (the `# Bucket:` header +
require_app_member/admin_required decorators). Restored so the authz structural
sweep + hardening matrix pass again, using the recovered rbac primitives
(app_access_tier / require_app_member).

- env_vars: can_access_app (read) / can_edit_app (write) across handlers
- apps sleep/scale policy, container_status, snapshots, git deployments/webhooks
  (visibility-scoped), databases raw-docker routes (admin), status-page checks
- wordpress: sealed reads + P0 destructive sealed-write + convergence reads +
  standalone host-ops admin-gated
- bucket headers on the already-gated app_volumes/deploy/domains/private_urls/
  python/security/templates

test_authz_structural_sweep 2, test_api_authz_hardening 27, test_git_webhook_authz
5 — green; 430+ route regression tests still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 02:05:50 -04:00
Juan DenisandClaude Opus 4.8 98d05eecdf fix(recovery): gpu core-slim move + cross-service doctor/schedule wiring
Cluster G of plan 38 (plan 32) + integration glue.

- gpu: complete the core-slim move — remove core app.api.gpu +
  app.services.gpu_service (+ its blueprint registration), rebuild the probe in
  the serverkit-gpu builtin extension (nvidia-smi parser, Linux-guarded)
- __init__: register dns_cutover + setup_health blueprints
- doctor_service: add fleet DNS-vs-IP checks, backup-proof checks
  (backup_drill_stale/backup_unverified), and the setup-health section into run()
- builtin_handlers: seed the host-doctor sweep + weekly setup-health nag
- test_jobs: schedule count 15 -> 17 for the two restored schedules

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 01:41:54 -04:00
Juan DenisandClaude Opus 4.8 b29fa77313 fix(recovery): rebuild authz-R2 policy + notification digests/chat webhooks
Clusters E+H of plan 38 (plans 24/29/33).

- security_policy_service: require-2FA policy (ok/nudge/enforce), SSO-exempt,
  grace window (plan 29); restored can_operate_app/can_admin_app on
  resource_grant_service for the member-action callers — 23 tests green
- notifications/digest: per-user daily/weekly digest rollups over queued_digest
  deliveries, respecting cadence + last_sent_at (plans 24/33)
- notifications/service: deep-link action_path/action_label on send + inbox
  category/severity filters (plan 24 Phase 5); catalog link builder
- chat_webhook_service: org chat/webhook connections, HMAC-signed delivery,
  category fan-out through the notify bus (plan 33)
- restored digest_cadence/digest_last_sent_at on NotificationPreferences

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 01:41:43 -04:00
Juan DenisandClaude Opus 4.8 a131cf5e0f fix(recovery): rebuild DNS cutover service+API and SSH site importer
Cluster C of plan 38 (plan 31). dns_cutover_service reconstructed from readable
string fragments + plan doc + the frontend contract; SSH importer from the
surviving wiring tests.

- dns_cutover_service: server-sourced snapshots, dry-run + real cutover via the
  guarded provider client, world-restoring revert that deletes cutover-created
  records (uses restored DnsCutoverSnapshot) — 20 tests green
- api/dns_cutover: 7 JWT/admin-gated endpoints matching dnsCutover.js
- site_importers/ssh: GenericSshImporter + parse_ssh_source, keyfile-only,
  credential-scrubbing; wired into site_import_service ('ssh' source) — 33 tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 01:41:22 -04:00
Juan Denis 6e32519f7e restore: remaining reconstructed July 5-7 changes (clean recovered files not attributable to one plan) 2026-07-07 20:29:31 -04:00
Juan Denis 38c281d274 feat(cron): completion — run history, project scope, cwd/run-as (plan 34) 2026-07-07 20:29:29 -04:00
Juan Denis a882d35db9 feat(adoption): DNS cutover+verify, snapshots, SSH import (plan 31) 2026-07-07 20:29:26 -04:00
Juan Denis 44892d3314 feat(agent): V2 capability pack — survey/doctor/fleet sweep+repair (plan 28) 2026-07-07 20:29:24 -04:00
Juan Denis 2eabfac2b8 feat(onboarding): setup health, preflight + reconcile (plan 22) 2026-07-07 20:29:23 -04:00
Juan Denis 782ea70c8b feat(workspace): scoping audit + per-scope matrix (plan 19) 2026-07-07 20:29:22 -04:00
Juan DenisandClaude Opus 4.8 315c80e9a0 feat(manifest): apply engine — resolve/plan/apply + core domain attach (Phase 2)
Add manifest_apply_service (resolve -> ordered plan -> idempotent apply inside a
manifest.apply DeploymentJob with before/after snapshots), covering app create/
update, env, volumes, managed databases, backup policies and domain attach.
Extract the WordPress one-call domain+SSL pattern into a core DomainAttachService
(DNS -> vhost -> best-effort cert, each stage degrading to a warning). Wire the
/api/v1/manifests blueprint with get/plan/apply (admin-gated, audit-logged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 01:28:16 -04:00
Juan DenisandClaude Opus 4.8 ae0f823e90 feat(manifest): persist manifest + seed env + health-check gate (Phase 1)
Stop dropping what import already detects. Add Application.healthcheck_path
(migration 054) and an application_manifests table (one row per project) storing
the normalized manifest, hash and provenance. Import now seeds non-secret env
values and records the health-check path, and the zero-downtime restart gate
polls that path instead of a blind sleep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 01:16:29 -04:00
Juan DenisandClaude Opus 4.8 8c5ede0cfd feat(manifest): v1 serverkit.yaml spec, normalizer, scaffold (Phase 0)
Add the declarative serverkit.yaml v1 spec: JSON Schema + reference doc, a pure
parse/validate/normalize service, version:1 recognition in the repo manifest
detector (legacy flat files unchanged), and a scaffold endpoint that renders a
manifest from a live app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 01:11:50 -04:00
Juan DenisandClaude Fable 5 75a839f556 fix(dns): move DNS provider routes back to core
The /api/v1/email/dns-providers routes lived only in the serverkit-email
extension, so panels without it (email is gated to mail users) got a
404/405 from the Settings -> Connections DNS tiles. The service was
already core; the routes now are too, at the same historical paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:18:08 -04:00
Juan DenisandClaude Fable 5 8ff893a4c8 feat(hosting): .htaccess to nginx converter
Paste-in translator for common rewrite/redirect/auth/access rules with
per-line unsupported flags, launched from the Import wizard (no core
custom-rules editor exists yet — output is copy/insert).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:22:05 -04:00
Juan DenisandClaude Fable 5 96d162e9a4 feat(hosting): per-site nginx micro-cache toggle
Opt-in 10s FastCGI/proxy micro-cache per site (migration 053) with safe
bypasses for logged-in/admin/cart cookies and paths, X-SK-Cache header
for verification, and a shared-zone purge. Flows through the drift-safe
vhost render seam so detection and writes always agree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:21:50 -04:00
Juan DenisandClaude Fable 5 3cbbcb5e6b feat(hosting): per-domain bandwidth accounting
Daily nginx access-log aggregation (migration 052) into per-site
transfer stats with 30-day sparklines on the Services list and a
monthly figure on the service Overview. Streaming parser handles
combined and vhost-prefixed formats plus rotation spillover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:21:48 -04:00
Juan DenisandClaude Fable 5 c192e4f383 feat(security): job-backed malware scans with a web-shell YARA pass
Malware scans run as jobs and can target an app's docroot; a curated
web-shell rule set (yara CLI when present, pure-Python fallback) runs
alongside ClamAV with custom .yar upload; quarantine gains restore via
original-path metadata. Note: api/security.py also carries the FIM
routes of the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:21:23 -04:00
Juan DenisandClaude Fable 5 c3f356b6fb feat(cli): API-backed operator verbs and diagnostic support bundle
serverkit status / services / apps / doctor [--repair] / repair /
update / login-url / support-bundle. Break-glass auth mints a 10-minute
admin JWT in-process (audit-logged) — root on the box already implies
full control. The support bundle exports a secret-scrubbed zip of
versions, service states, setting shapes and recent job failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:21:22 -04:00
Juan DenisandClaude Fable 5 f296e1dd61 feat(ops): config drift detection, per-resource repair and doctor sweep
Daily read-only drift sweep re-renders expected nginx vhosts and compose
overrides from panel state (via extracted pure render seams) and diffs
against disk; drift notifies admins. Doctor tab on Monitoring runs the
full diagnosis (drift, services, certs, disk, DB) with diff-confirmed
per-item and batch repair. Nothing auto-repairs. The nginx render seam
also carries the micro-cache blocks used by the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:21:09 -04:00
Juan DenisandClaude Fable 5 d01d82036b feat(imports): panel-migration pipeline with cPanel, DirectAdmin and Hestia importers
Format-pluggable analyse->plan->run pipeline (migration 051) that maps
legacy panel backups to ServerKit apps, managed DBs (preserving MySQL
password hashes where portable) and cron entries, as resumable jobs
with per-step logs, SSRF/tar-traversal guards, and a 5-step Import a
Site wizard at /imports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:24:29 -04:00
Juan DenisandClaude Fable 5 76a688fb49 feat(databases): managed DB users and one-click Adminer SSO
managed_database_users rows (migration 050) track users/grants so they
survive restarts; SSO launches an on-demand Adminer container
pre-authenticated with a single-use, 5-minute shadow credential scoped
to one database, reaped on schedule. Also mounts the config tuner in
the explorer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:24:15 -04:00
Juan DenisandClaude Fable 5 1fb7b81c32 feat(databases): curated engine config tuner
Vetted MySQL/PostgreSQL settings with RAM-aware suggestions, applied
via a ServerKit-owned drop-in (MySQL) or ALTER SYSTEM (PostgreSQL) with
timestamped backups, verified restart and clean rollback. Never
auto-applies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:24:03 -04:00
Juan DenisandClaude Fable 5 0a82873c83 feat(databases): live processlist with kill in the explorer
SHOW FULL PROCESSLIST / pg_stat_activity normalized per server or
container, with admin-only kill/terminate and 5s auto-refresh tabs in
the Database Explorer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:24:02 -04:00
Juan DenisandClaude Fable 5 bd7624130c feat(auth): one-time login links and demo mode
Single-use, short-TTL, optionally IP-bound login URLs (hash-at-rest,
migration 049, hourly reap job) minted from Settings > Users; plus a
config-gated demo mode that 403s mutating API calls and seeds read-only
credentials on the login page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:23:50 -04:00
Juan DenisandClaude Fable 5 9edeb68f85 feat(apps): first-class per-app CPU/memory limits
cpu_limit/memory_limit on Application (migration 048), emitted into the
generated compose and the ServerKit override for live re-apply, with a
Resource Limits panel (usage vs limit) in Service Settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:23:39 -04:00
Juan DenisandClaude Fable 5 e7e2de486f feat(monitoring): on-demand server speed test card
Job-backed speed test (Ookla/speedtest-cli when present, pure-Python
fallback) with download/upload/latency surfaced on the Monitoring
overview; last result persisted in settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:23:29 -04:00
Juan DenisandClaude Fable 5 4c76d43cb7 feat(security): cloud-metadata egress guard for app containers
Default-on (opt-out) DOCKER-USER drop rule for 169.254.169.254 with an
nftables fallback, toggleable from Security > Firewall. Kills the
SSRF-to-cloud-IAM credential-theft class on cloud VPSes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:23:11 -04:00