build-release.sh excluded *.png/*.jpg/*.jpeg globally — fine when the only
images were screenshots, fatal once ExtensionIcons.js (8bd9c1a) started
importing frontend/src/assets/extension-icons/*.png as build inputs: the
/tmp build tree lost them and rollup failed to resolve. The blanket rule
was also silently stripping frontend/public/favicon-*.png from every
release build.
Replace with targeted excludes: docs/screenshots, frontend/dist,
.codex-screenshots, template-icons, and (via --no-wildcards-match-slash)
root-level ./*.png|jpg|jpeg strays + dev/*.png. Verified by listing the
tar: 14 extension icons + 4 favicons included, zero stray images, *.log/
*.tmp/*.pyc still excluded; shellcheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The plan-39 em-dash purge hand-wrote six baked plugin.json copies without
the trailing newline that sync-builtin-frontends.mjs emits, tripping the
CI drift gate (--check). Regenerated via the script; content unchanged.
Guard suite test_extension_manifests.py: 28 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce serverkit-gpu extension: backend plugin with GPU API and service (nvidia-smi based) providing per-GPU metrics and best-effort container resolution for GPU processes. Files added: backend/app/plugins/serverkit-gpu/{__init__.py,gpu.py,gpu_service.py,plugin.json}. Also update several frontend plugin.json files (including serverkit-gpu) to match manifest formatting (trailing newline adjustments). The GPU service degrades gracefully when nvidia-smi or Docker info is unavailable.
Install the guard the data loss taught us to want: code and its proving tests
died together, so the suite stayed green (2572->2209 passing) while whole
features silently vanished — nobody diffs a green number.
- backend/tests/BASELINE_COUNT (2475) + check_test_count.py: compares pytest's
collected count to the floor; CI (backend-ci.yml new step) fails if it drops.
Lowering the floor requires editing BASELINE_COUNT in the same commit, so a
silent test loss surfaces as a red X instead of a still-green suite. Verified:
passes at baseline, exits 1 on a simulated drop.
- CHANGELOG: document the recovery-parity restore (Cloudflare R2 engine,
email-bounce, DNS-cutover staging, survey routes, cron CSS, 18 test suites)
and the ratchet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Recover three frontend losses from the 2026-07-08 rebuild:
- _cron.scss: merge-restore the dropped .scheduled-tasks-card/-runs run-history
drawer + .cron-jobcell__name rules from the clean shadow (46 -> 75 blocks);
restored ONLY classes still referenced in JSX (no dead CSS), kept the plan-39
.cron-kpis strip.
- SurveyTab.jsx (plan 28): rebuilt against the surviving survey API + wired into
ServerDetail's tab strip (capability-gated), and fixed main.scss which never
imported _server-survey.scss (styles had never compiled). Read-only; the
recovered site-migration/decommission bits were dropped (those APIs don't
exist in the current backend).
- runtime/__tests__/{sdkGate,sha256}.test.mjs (plan 32): re-created as node:test
units against the surviving sdkGate.js/semverRange.js + digest.js seams.
Verified: node --test 9/9 pass, npm run lint 0 errors, npm run build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
app/api/survey.py defines survey_bp + its /api/v1/servers/<id>/survey routes
(plan 27/28) and its own docstring says "Mounted at /api/v1/servers (see
app/__init__.py)", but the 2026-07-08 recovery rebuild dropped the
register_blueprint call, leaving every survey/management-mode/observed-status
route a 404. The survey_service logic survived intact. Re-register it under
/api/v1/servers (same prefix as servers_bp, no route conflict). Un-blocks the
restored test_survey_api catalog/snapshot route coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-arm the proving surface the data loss silently took: 18 backend test
files reconstructed from winfr-carved .pyc fragments (marshal+dis extractor
for clean pycs; string-salvage + surviving-source reconstruction for
fragmented ones). Restores coverage for backup drills/offsite (plan 30),
fleet sweep + fleet-doctor DNS + DNS cutover + ssh import + observe
enforcement (plans 26/31), survey catalog/api + setup security + workspace
scoping (plans 28/22/19), and sdk contract + cron preview + notification
prefs + runtime frontend (plans 32/25/34/24).
Where a restored test proved its feature HOLLOW (code died with its test),
the affected cases are @pytest.mark.skip(reason="plan 42: ...") naming the
exact missing symbol, so the canary stays green while flagging the gap for a
named follow-up. Full suite: 2373 passed, 56 skipped, 0 failed; collected
2322 -> 2429. test_gpu dropped as redundant (coverage survives in
test_gpu_extension.py post core-slim).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a CSS rule to _marketplace.scss to right-align the .plugin-install-form__docs element inside the plugin install form. This ensures the docs link/button is pushed to the far right for improved layout and spacing in the marketplace plugin card.
Add a fromServer env binder ({property: publicIp|hostname}) resolved against the
target server or panel host, with a fromserver_no_ip plan-time blocker. Add the
${SERVER_PUBLIC_IP} template magic variable. Attach manifest-generated app
projects to a shared external `serverkit` docker network (DockerService.
ensure_network + networks param on create_docker_app/buildpack generate_compose)
so a fromService host reference resolves cross-app by container name at runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add first-class `image:`/`registry:` (apply stamps Application.docker_image +
registry_id, resolved by registry name; registry_credential blocker on
unknown/uncredentialed registries, ECR accepted) and `hostRequirements`
(privileged/capAdd/sysctls/devices/kernelModules). create_docker_app emits the
host-requirement fields; every requirement is listed in plain words as a plan
issue and audited via manifest.host_requirements on apply. kernelModules get an
advisory /proc/modules check that warns but never silently passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `containers:` map that turns one service into one Application rendered as
one compose project. Normalizer validates mutual-exclusion with buildpack keys,
dependsOn references, and rejects cycles (DFS). New UnitComposeService renders
container_name prefixing, healthcheck blocks (cmd->CMD-SHELL, httpPath->wget),
depends_on.condition mapping, raw port publishes, and per-container synthetic
named volumes. resolve_service flattens container ports/bootstraps/host-reqs/env
to the unit (unified `bootstraps` list under one bootstrap_done flag); unit disk
backups route at the docker-volume name. Seams: unit_compose/unit_container_names.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
- Per-source explainer strip (SOURCE_GUIDES): the selected source card renders
what it does, what to have ready, and the 2-3 next steps.
- Extract the hardcoded SERVICE_TEMPLATES into frontend/src/data/deployTemplates.js;
the Templates tab gains a 'Deploy templates' section (badged 'Example' for the
demo) with a 'Use template' deep-link (/services/new?template=<id>) that
preselects it in the wizard. One list, two entry points; Template Library
button now targets #deploy-templates.
- New docsLinks.js map + <DocsLink> (book icon, opens serverkit.ai docs in a new
tab, hidden under White Label — the single place that rule lives). Wired into
the New Service source pane (deploy-sources / manifest) and the Marketplace
manual-install modal (extensions/installing).
Lint 0 errors; build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EXTENSIONS.md gains an 'Installable straight from GitHub' section: plugin.json
at archive root, release .zip preferred over zipball fallback, accepted URL
forms incl. owner/repo[@tag], and how the preview/consent flow presents your
declared permissions (checksum-pinned install). Keeps the GitHub docs and the
serverkit.ai Installing page telling the same story.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single home for the schema on the product domain (serverkit.dev -> serverkit.ai
in SERVERKIT_YAML.md examples and the schema $id), matching the new
serverkit.ai/serverkit-yaml.schema.json endpoint. Also documents the deferred
registry-URL flip (plan 40 task 11): DEFAULT_REGISTRY_URL stays raw-GitHub
until the serverkit.ai /ext endpoints are deployed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
scripts/export-registry-entries.mjs reads builtin-extensions/*/plugin.json
and emits index-v2 bundled entries for the serverkit-extensions registry.
Normalizes panel categories onto the registry enum and optionally attaches
repo-relative logo paths when art exists. Run and paste into index.json via
PR whenever the bundled set changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Marketplace:
- Bundle 14 illustrated extension cover icons (ExtensionIcons.js resolver);
render on a clean light cover, larger & centered (no gradient/white-chip clash)
- Move search into the page top bar and add a reusable schema-driven FilterDrawer
(SearchField + FilterDrawer/FilterButton ds primitives); drop the always-on
publisher segmented control and the "N results" count
Jobs:
- Convert to a tab-group page (Activity / Scheduled) so scheduled jobs are a tab,
not a scroll; reuse the shared SearchField + FilterDrawer in the top bar
KPI fixes (lost in the recovery rebuild):
- Add missing grid rules: .mon-kpis, .fleet-kpis, .ftp-kpis (stacked full-width)
- .sk-kpi__val gets an explicit color: var(--text) so values aren't near-black
on non-tabgroup shells
Terminal:
- List Log Files first so the default landing matches the highlighted tab
- Drop the top padding so the topbar pins flush like the other pages
gitignore: allow the bundled extension-icons PNGs (real Vite-imported assets).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_copy_files_uses_staged_docroot drove _step_create_app, which provisions
the app root under paths.APPS_DIR (defaults to /var/serverkit/apps). That path
is writable on a local dev box but denied on the CI runner, so the test failed
only in CI with PermissionError. Monkeypatch app.paths.APPS_DIR to the tmp
import base, matching the apps_dir pattern already used in test_site_import.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move serverkit-gpu from CONVERTED_BUILTIN_SLUGS into GATED_BUILTIN_SLUGS behind
a _gpu_present() predicate (shutil.which('nvidia-smi')), mirroring the email
gate: fresh installs stay marketplace-only, upgraded panels without a GPU no
longer get it, upgraded panels with a GPU keep it (D3 continuity). Rewrite the
12 builtin + baked extension descriptions that used em dashes into plain
sentences, keeping every pair in sync. New test_extension_manifests.py walks
both manifest trees to assert no em/en dashes and per-slug description parity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote Installed to a real top-bar tab (/marketplace/installed, route-driven
via useLocation; legacy ?tab=installed still resolves) and drop the inner Tabs.
Collapse the two chip rows + SectionHeader into one .marketplace-toolbar row
(search + category Select + publisher SegControl + reset + count). Compact the
cards (one badge row, 2-line clamp, tighter grid/padding, ~5 cols) and give
installed rows a matching density pass. Add ExtensionBrands.jsx: deterministic
per-slug gradient covers with brand marks, with a logo/brand/manifest-icon/
category fallback chain, plus optional registry logo passthrough.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The recovery-rebuild Email.jsx emitted sk-email__* classes while _email.scss
still styled the dead .email-page selectors. Rewrite _email.scss for the
sk-email namespace (drop the dead block), give the not-installed state a
designed EmptyState hero with a deliverability-preflight note, and move the
services/accounts/queue tables to DataTable with client-side search on the
account and queue lists.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Commit 6e32519 (the July 5-7 recovery restore) wrote git-reflog garbage
over scripts/install.sh (the agent installer served at
GET /api/v1/servers/install.sh) and whitespace-mangled content over
scripts/test/full-stack-test.ps1, breaking scripts-ci: shellcheck SC2148
(no shebang) and 15/18 test_agent_install.sh failures. Restore both files
verbatim from 6e32519~1, the last good revision.
Verified: test_agent_install.sh 18/18, test_install.sh 62 passed,
shellcheck --severity=error clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add formatCompact/formatFull (Intl.NumberFormat) util, an opt-in `compact`
prop on MetricCard (value >= 1000 renders compact with the exact number as
the hover title), and CSS hardening (tabular-nums + truncation guard on
.sk-kpi__val, minmax(0,1fr) rail grid). Apply to the Queue Bus rail tiles
and status/group nav count badges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove the flex spacer that bottom-pinned the About nav item, leaving a large
empty gap; About now sits directly under White Label in My Account.
- System Info's System Details card now shows the ServerKit (panel) version,
white-label-aware (uses the brand name when white-labeling is enabled) — it
previously showed host/OS details but never the panel version.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The staging verify's migration-head check grepped for Alembic hex hashes
([0-9a-f]{8,}), but ServerKit revision IDs are descriptive slugs (e.g.
074_cf_ops_changes). The empty grep exited non-zero and, unguarded under
set -e, aborted verify before the VERDICT line ever printed. Take the first
token of `flask db current`/`heads` instead, and guard with `|| true`.
Surfaced driving plan-38 Phase 5 (staged deploy of the recovered dev to the DO
test box via Faro): deploy migrated cleanly to head 074 but verify crashed
pre-verdict. With this fix, `verify --full` emits a green VERDICT (pass:true).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>