A template file with a relative path (prompture-hub's Dockerfile) was
bind-mounted as './Dockerfile:Dockerfile', failing compose up with
'invalid mount config: mount path must be absolute'.
- deployment_job_logs.id: BigInteger -> with_variant(Integer, 'sqlite');
BIGINT PK never autoincrements on SQLite, so the first log insert crashed
every fresh install (jobs stuck 'running' at step 0 with no logs)
- migration 076 rebuilds the table on existing SQLite databases
- runner: first status/log/telemetry inside try, non-raising log(),
rollback-safe failure marking — any failure now fails the job visibly
- service: guard enqueue, crash-proof run_job, fail jobs left 'running'
across restarts at boot
- repo-based services (POST /apps/from-repository) now enqueue a real
'app_deploy' DeploymentJob (build + start via DeploymentService.deploy)
and return deploy_job_id
- compose override injects restart: unless-stopped for services without a
policy, so apps come back after reboot
- GET /deployment-jobs gains an app_id filter
- frontend: shared DeploymentJobProgress component (timeout + visible poll
errors) used by the Templates InstallModal and the service Logs tab;
repo wizard navigates to the Logs tab to watch the deploy live; install
success lands on the Logs tab; failure toasts surface real server errors
The LocalKit contract for per-kind sync (plan 26): /pair now returns a `kinds`
array (site kinds this extension can sync) and every /sites entry carries a
`kind`. Both keep old-server ↔ new-client safe — a LocalKit client defaults an
absent kind to "wordpress" and gates its php sync/import UI on the `kinds`
advertisement.
KINDS is ['wordpress'] today: serverkit-wordpress is this extension's only site
backend, so advertising php would be untrue. Add 'php' here in lockstep with a
php-stack backend that _resolve_site / _install_code / _import_db can dispatch
to; the client already speaks the php protocol (engine-native db, app/ code).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flip DEFAULT_REGISTRY_URL to https://serverkit.ai/ext/index.json (the
long-planned go-live default: cached proxy of the raw index with logo URL
rewriting). Add logo fields to the bundled offline index so Marketplace
cards get real art even before the first successful fetch, and update the
registry tests + docs for the new default.
DeploymentRunner.run() logged self.job.target_server_name, but the model
only exposed that value as a to_dict() key, so every deploy job crashed
with AttributeError at its first step and was marked failed before any
work ran. Add a target_server_name property and reuse it in to_dict().
Introduces a full-stack Test Sandbox feature to run distro matrix tests in Docker from the UI: new backend API/service, sandbox_runs model + migration, run/log endpoints, and a new frontend page with mode selection, run history, live polling, cancel, and per-distro logs.
Also adds ARM64 scripts CI coverage (ubuntu-24.04-arm) and strengthens install/test automation for minimal images and overlay rebuilds: early curl/bootstrap index refresh, stricter venv validation, release-venv glibc fallback, Node reprovisioning on source fallback, rsync --delete overlay behavior, npm bootstrap in VM install, Windows tar pinning in PowerShell harness, plus updated test docs and distro coverage notes.
Sync v1 POSTs the whole payload in one request, so it is capped by the
panel's 100MB MAX_CONTENT_LENGTH and a drop at 99% means starting over.
v2 makes every request one 8 MiB chunk, which lifts the cap and makes a
retry cost only what was actually lost.
- POST /push/<kind>/init: describes the transfer, reaps stale ones, and
returns the offsets already confirmed for an identical payload — that
array IS resume.
- PUT /push/<kind>/chunk: per-chunk sha256, idempotent re-sends, marker
written after the bytes so a crash costs one chunk, not a corrupt file.
- POST /push/<kind>/finish: verifies the whole-file sha256, THEN runs the
processing. Missing chunks are a 409 with the received set (resumable);
a hash mismatch discards. Because processing only ever runs on verified
bytes, an abandoned transfer cannot leave half-applied state.
- push/code and push/db v1 now delegate to _install_code/_import_db, so
v1 and v2 share exactly one processing path and the safe-extract policy
stays mandatory in both.
- Pulls accept ?session= and serve with conditional=True, giving Range /
If-Range. The session pins one materialized export: pull/db and
pull/code build their payload per request, and ranges taken from two
different mysqldump/tar runs would assemble into neither. No session =
v1 export-and-delete, unchanged.
A transfer is a directory under the temp dir with per-chunk marker files
rather than a shared JSON list, so concurrent chunk writes need no lock.
Transfer ids are validated against uuid4().hex before touching a path.
FEATURES gains 'sync-v2'; older clients keep the v1 routes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The download half of code sync — the missing piece for LocalKit's
"import this remote site as a new local site" flow (LocalKit plan 18).
- GET /pull/code streams a tar.gz of the site's wp-content, built inside
the container with `docker exec tar czf -` so permissions and structure
survive. Entries are prefixed `wp-content/`, the same format POST
/push/code accepts, so both directions share one archive shape. GNU
tar's exit 1 ("file changed as we read it") is tolerated — a live site
writing uploads mid-archive is normal.
- GET /pair now reports a `features` array so clients gate their UI on
capability instead of discovering a 404 mid-operation.
- GET /sites gains `php_version` (regexed off the compose image tag, not
a per-site container shell) and an explicit `site_url`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make dependency scanning reliable in CI by adding a blocking pip-audit job for the backend and test harness requirements, while keeping Safety as a non-blocking advisory check because third-party service failures should not block merges. Also bump `click` to `8.3.3` to address the reported vulnerability in a directly pinned dependency.
Extracting the seven builtins to their own repos took their suites with them —
2722 collected tests down to 2430. The ratchet fired exactly as designed, since
a 292-test drop is indistinguishable from the silent loss it was built to catch;
lowering the floor is the explicit statement that this removal was intentional.
Those tests are not gone, they moved: each extension's suite now lives in its
own repo and runs in that repo's CI. The four panel-core cases rescued from
test_tramo_extension.py stay here in test_tramo_panel_core.py.
This should have been part of the extraction commit, which is where the script's
own guidance points. It lands separately because the extraction was already
pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds USDT (TRC-20 and ERC-20), BTC and SOL to the Support section of all four
READMEs, with the network in its own column — USDT exists on several chains
whose addresses look alike, and the network is what tells a donor which one to
send on.
QR codes come from scripts/generate-funding-qr.mjs rather than a web generator,
since an online tool sits in a position to encode a different address than the
one entered and the resulting image looks perfectly normal. The script holds the
addresses as the single source of truth and checksum-validates each before
encoding — base58check for Tron, EIP-55 casing for Ethereum, bech32 for Bitcoin,
32-byte base58 for Solana — refusing to emit anything if one fails. Every code
was decoded back and diffed against its source string. `--check` re-verifies the
committed PNGs so an address edit with a stale QR cannot pass unnoticed.
These are self-custody wallets rather than exchange deposit addresses, which can
be rotated out from under a published README and turn third-party donations into
an AML review on the receiving account.
Adds CODEOWNERS over the READMEs, the QR script, the funding images,
FUNDING.yml, LICENSE and SECURITY.md. Swapping a donation address through an
innocuous-looking docs PR is a known attack on open-source projects; this
requests maintainer review on every path that carries one. It takes effect once
"Require review from Code Owners" is on in branch protection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A blanket `*.png` rule meant the eight diagrams added alongside the
ARCHITECTURE.md rewrite were silently skipped — `git add` reported nothing and
the doc landed referencing images that were not in the repo. Every diagram
rendered as a broken image on GitHub.
Force-includes docs/images/ the same way docs/screenshots/ and the extension
cover icons already are, covering both the architecture diagrams and the
funding QR codes that follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
English: drops the retired Workflow Builder — its screenshot showed a sidebar
entry and canvas that no longer exist — promoting Agent Fleet into that grid
slot, and renames the feature card to Automations with the managed-container
model it actually uses. Template counts go 60+ to 100+, the architecture section
leads with a diagram over an ASCII-only block, and the Tech Stack no longer
claims GeventWebSocket workers. Two links to agent/README.md are repointed at
the serverkit-agent repo; that directory left this tree when the agent was
extracted, so both were 404s.
The translations had drifted a full generation behind. All three pointed their
Screenshots sections at ad-hoc user-attachments URLs instead of the repo's own
files, and their roadmaps still listed Backups, Mail, RBAC and Marketplace as
unshipped. Each is now a faithful mirror of the English: identical section order
and count, 121 table rows, 41 images, both collapsible folds, and nav anchors
that resolve under GitHub's slugger with accents and CJK preserved. The ASCII
architecture diagram is byte-identical across all four — deliberately untouched
in the Chinese file, where double-width characters would break the box drawing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The document described a system that partly no longer exists. Removed: the
Workflow Automation section built on a `WorkflowService` that is not in the tree
(Automations replaced it and runs in a managed container), and the claim that
DB credentials are injected as DB_HOST/DB_USER — there is no generic injector,
names are template-authored and the real mechanism is fromSecret/fromService
reference resolution.
Corrected: backups live at /var/backups/serverkit not under /var/serverkit,
/var/serverkit/ssl does not exist, the panel has no config.yaml (only the agent
does), templates merge two directories with backend/templates/ as the source of
truth, and 8000 is the fallback port rather than the start — managed_app_base_port
and the per-template default both take precedence.
Added the three subsystems that were missing entirely — the extension platform,
jobs and scheduling, and the notifications bus — plus a backend layer map and an
agent fleet section absorbing the single-worker constraint.
Every diagram now leads with an image and keeps its ASCII fallback in a
collapsible fold, so the doc still reads as plain text.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
File counts were roughly a third of reality (api 36 vs 90, services 48 vs 165,
models 15 vs 80, pages 29 vs 60), so both files understated the codebase enough
to mislead anyone sizing a change.
Two claims were wrong rather than stale. `db.create_all()` is never called —
schema is 74 Alembic migrations, and describing tables as auto-created invites a
model change that ships without one. And the Production Build section
recommended GeventWebSocket workers while the deployment note 25 lines above
warned against exactly that; the Dockerfile and systemd unit both use a plain
threaded worker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The manual-install systemd unit told operators to run
`--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker`. The app
runs `async_mode='threading'`, where engineio serves WebSocket through
simple-websocket; the gevent-websocket worker also answers the upgrade, so the
handshake is answered twice and the browser reports "Invalid frame header".
Anyone following this guide built a panel with broken live metrics, logs and
terminal.
Now matches what the project actually ships in
templates/serverkit-backend.service.in — `--workers 1 --threads 100`, no worker
class — with a note on why to scale threads rather than workers, since the agent
gateway keeps its registry in one process.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The README has carried an MIT badge and a link to ./LICENSE since the first
commit, but the file was never added — the badge 404'd and the project had no
license text, which legally means all rights reserved regardless of intent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every planned core phase has shipped. New functionality now arrives as
installable extensions through the Marketplace, and core work is limited to
stability fixes and UI polish. Reframes the "what's next" list accordingly and
points feature requests at the extension SDK with an `extension-idea` label,
keeping the phase history as the record of how the core was built.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adding a variable happened through an inline row that competed with the list
for attention; it now opens a dedicated modal that resets its own state on open.
Drops the is_secret toggle. Env vars are stored as secrets unconditionally, so
offering a choice implied a public mode that does not exist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PageTopbar measured overflow against a region that grew and shrank with the
page title, so a title change shoved the tabs sideways on first paint. The nav
is now the flex-fill measurement region, right-aligned so the title can resize
without moving the tabs, with overflow hidden to suppress the pre-measure flash.
The tab strips themselves were a transparent underline that dissolved into the
page background. Service detail, applications and WordPress detail now share one
treatment — pills inside a bordered surface, active pill filled — so every
detail page's nav reads the same. Service detail also gets a consistent vertical
rhythm between the KPI band, the info grid and the full-width panels, fixing the
KPI strip being glued to the section below it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generated manifests now contribute both `<slug>` and `<slug>/:tab`, forwarding
every sub-path to the same component. Adding a tab to a scaffolded extension
becomes a one-line change to TABS in frontend/index.jsx instead of a manifest
edit plus a route entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend-only builtin that pairs the panel with LocalKit, the local WordPress
companion app. Ships as a marketplace one-click builtin (no frontend surface),
so it appears in the Marketplace but adds nothing to the sidebar until a user
installs it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Analytics, CrowdSec, DNS Server, GPU Monitor, Kubernetes, Mail Server and
Automations (tramo) now live in their own repositories and install from the
registry as versioned, sha256-pinned zips. Their sources leave
builtin-extensions/, their pre-bundled frontends leave frontend/src/plugins/,
and registry_index.json gains a `bundled: false` entry for each so the
Marketplace offers them as normal registry installs.
The panel keeps only the wiring: comments at the former mount points explain
where each surface now comes from, and the gpu auto-install gate in
extension_migration.py is dropped since a host-probe can no longer decide to
re-vendor an extension that is not in this tree.
Also untracks backend/app/plugins/serverkit-{gpu,k8s,tramo}/, which were the
panel's own install artifacts rather than source. Committing those shipped a
second copy of code that now lives in three other repos, guaranteed to drift.
.gitignore now excludes the install path for every extracted slug on both the
backend and frontend side, so a local install can never be committed back.
Four tests from test_tramo_extension.py exercise panel core rather than the
extension (the retired /api/v1/workflows blueprint, the plan-45 event emitters
ported to EventService, and the docker_service double-emit fix); they move to
test_tramo_panel_core.py and stay here. The rest of the extension suites go
with their repos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add useSelfCapturedBones(key, {ready}): after a page's real content renders it
snapshots the region into bones (shared src/utils/snapshotBones.js) and caches
them in localStorage under sk-bones:<key>, versioned so stale layouts are purged;
the next load replays a pixel-accurate skeleton. First-ever visit has no cache and
falls back to the composed skeleton — no regression.
SkeletonBoundary now forwards its ref to the measured container, and the SSL page
is wired as the demonstration. The capture script (phase 2) is DRY'd to inject the
same shared snapshotBones source rather than an inline copy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an accuracy path for skeletons: a dev-only `npm run capture:skeletons` tool
(scripts/capture-skeletons.mjs) drives a headless, logged-in dev session, measures
designated page regions into boneyard-format bones ({x,y,w,h,r}; x/w %, y/h px),
and bakes them to frontend/src/skeletons/*.json as static assets. Nothing browser-
or capture-related ships in the product.
SkeletonBoundary gains an optional `bones` prop that replays a captured layout via
a new `renderBones` mapper — SCSS-classed absolutely-positioned placeholders reusing
the shared .skeleton look (shimmer + reduced-motion). boneyard-js is added as a
devDependency for parity/format reference (never imported by shipped code). Ships a
README runbook and an example.json documenting the format.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deprecate the Tailwind `ui/skeleton` shim (deleted; its two call sites moved to
the SCSS `Skeleton` primitive with no raw Tailwind sizing classes) and add
`SkeletonBoundary`, which keeps real content in flow and paints the skeleton over
its exact box while loading — inheriting the real dimensions so there is no
cumulative layout shift when data arrives. Falls back to intrinsic size when the
data-gated children are still absent (first load), overlays once content is
present (refresh).
Migrated the worst hand-guessed loaders through the boundary: SSL certificates,
WordPress list + detail page, the WordPress activity feed, and all 11 WordPress
detail tabs. Skeleton shimmer now honors prefers-reduced-motion across the legacy
wp-* skeleton families too (specificity-proof override), and loading regions set
aria-busy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- docs/ANALYTICS.md: privacy model, the two data sources, dashboard tour,
snippet + WordPress mu-plugin + nginx sub_filter install paths, config table,
geolocation, scale/PostgreSQL guidance, collector protections, and the
single-worker note. Clarifies the panel-extension vs WP-plugin naming and that
the Umami/Plausible/PostHog templates remain available.
- Recommend serverkit-analytics for the wordpress + web-apps setup-wizard use
cases (RECOMMENDED_EXTENSIONS_BY_USE_CASE). Onboarding guard test green.
- CHANGELOG (Unreleased): Web Analytics feature entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- log_ingest_service.py: generalized apache/nginx combined-log parser (skips
assets, non-GET, and bots so log hits approximate real pageviews). Incremental
per-site cursors — file sources use (inode, byte_offset) with rotation/
truncation reset; docker sources use `docker logs --since <last_ts>` with a
ts-based boundary dedup. Events tagged source='log' so JS vs log stay
comparable. run_log_tail (scheduled) + ingest_site.
- geo.py: optional country-only GeoLite2 lookup, off by default, cached reader,
returns None on any miss (no package / no DB / private IP). Wired into JS
build_event and log ingestion; no IP is ever stored.
- analytics.py: admin POST /sites/<id>/ingest-logs (ops/testing trigger).
- 12 new tests (parser, asset/bot/non-GET filter, log events, since-dedup, file
cursor advance, disabled skip, run_log_tail + global toggle, endpoint, geo
off/no-db, country null default). 83 total.
The realtime Socket.IO namespace is intentionally deferred (a plan fast-follow);
the Realtime tab already polls.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- wp_integration.py: writes a ServerKit Analytics mu-plugin into a managed WP
site. Docker sites (the default template uses a named volume, not a bind mount)
get the file via `docker exec tee` into /var/www/html/wp-content/mu-plugins;
bare-metal sites via a host write + www-data chown. Idempotent, remove-on-
disable, feature-detects serverkit-wordpress, remove_all_injections on
uninstall.
- nginx_integration.py: injects an idempotent, marker-guarded sub_filter block
(rewrites </body>) into a managed app's vhost, following the WAF include-
injection precedent. Validates with nginx -t and REVERTS the file if the test
fails (never leaves nginx broken); reloads (never restarts).
- analytics.py: admin POST/DELETE /sites/<id>/inject/{wordpress,nginx}.
- AnalyticsTab.jsx: feature-detects the extension and shows a banner linking to
the persistent dashboard, keeping the on-demand log view as a complement.
- 18 new tests (mu-plugin PHP, WP inject/idempotent/docker-exec/remove/
remove-all, wordpress_available, nginx block insert/strip/idempotent/revert-
on-invalid/remove, admin-gated endpoints). 71 total. lint + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>