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>
- rollup_service.run_rollup: DB-side aggregation of raw pageview events into the
daily table (overall + page/referrer/device/browser/os/country dims), with
single-pageview bounce detection. Idempotent + backfill-safe (recomputes every
(site,date) with raw events).
- report_service: rollup-backed overview/pages/referrers/devices/timeseries with
a range parser (range= or start/end), plus realtime read from raw events.
Documents the sum-of-daily-uniques range caveat.
- site_service: tracked-site CRUD (create/update/rotate-key/delete with data
cascade) + hostname normalization.
- analytics.py: JWT'd endpoints — /sites CRUD + /sites/<id>/{overview,pages,
referrers,devices,realtime,timeseries} + /rollup. Viewer reads, admin
mutations (tramo/k8s/mail RBAC precedent).
- 15 new proving tests (rollup overall/dims/idempotent, overview/pages/realtime,
range parse, full CRUD, RBAC viewer-read/admin-mutation, 401 unauth). 54 total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Raise BASELINE_COUNT 2623 -> 2637 for the two new files added by plan 48
(test_trusted_client_ip.py +7, test_login_bruteforce.py +6). Full suite:
2584 passed, 53 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces a new `.coderabbit.yaml` with ServerKit-specific review guardrails, and expands env/security documentation for trusted proxy headers and per-IP login brute-force throttling. It also adds inline backend notes clarifying `request.remote_addr` behavior under ProxyFix for auth, cron, and pairing paths, and updates the frontend plugin manifest to remove the Tramo and K8s plugin entries.
Add an in-memory (single-worker) per-IP throttle that complements the per-user
account lockout: it stops password-spraying across many accounts from one IP
and prevents a single attacker draining the shared login rate-limit bucket.
- new app/services/auth_throttle_service.py — lock-guarded TTL map with
is_blocked / register_failure / reset (+ opportunistic memory sweep)
- config: AUTH_IP_MAX_ATTEMPTS (10) / AUTH_IP_WINDOW_MINUTES (15) /
AUTH_IP_BLOCK_MINUTES (15)
- wire into /login (block up front -> 429 + Retry-After BEFORE the password
check; register on bad user/password; reset on correct password),
/login-links/redeem, and the 2FA /verify endpoint
- per-user lockout and the flask-limiter 5/min decorator kept (defense in depth)
- tests: test_login_bruteforce.py (6)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rate-limit buckets, login lockout and audit-log source IPs hand-parsed the
leftmost X-Forwarded-For token — a client-controlled value — letting an
attacker rotate it to dodge per-IP limits or forge audit trails.
- config: add TRUST_PROXY_HEADERS / TRUSTED_PROXY_HOPS (+ _env_bool/_env_int)
- create_app: wrap in gated Werkzeug ProxyFix (rightmost trusted hops) before
the limiter/handlers so every remote_addr consumer sees the real client
- new app/utils/client_ip.py::get_client_ip() — the single trusted seam
- replace all six leftmost-XFF parsers (rate_limit, audit middleware,
api_key_auth, api_analytics, audit_service, ddns) with get_client_ip()
- deploy: root .env.example ships TRUST_PROXY_HEADERS=true (behind nginx);
backend/.env.example documents it off for dev
- tests: test_trusted_client_ip.py (7) incl. real flask-limiter bucket isolation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Updates the create-pr skill guidance to stop using conventional-commit prefixes in PR titles and require plain, teammate-friendly titles instead. It adds clear good/bad examples, keeps formatting constraints (capitalized first word, no trailing period, ~70 chars), and clarifies optional subsystem prefixes like "Backend:". It also documents that release bump behavior is driven by commit messages on dev, so bump intent should stay in commits rather than PR titles.
Replace the old detailed logo SVG with the simplified 32x32 brand mark used elsewhere in the UI. The new asset uses the current accent gradient and white server glyph so the logo stays consistent across the sidebar, auth flows, setup screens, and favicon.
PageTitleUpdater reads branding values (panelTitle/publicTitle) via useAuth, so it must be rendered as a descendant of AuthProvider. Previously it was placed above the provider tree, causing it to run without auth context.
The installer's node_ready() now gates on Node ^20.19.0 || >=22.12.0 (was >=18),
so the T3 assertion "node_ready true for node v18" was stale and failed CI.
Rewrite T3 to prove the real floor: accepts 20.19+/22.12+ (with npm), rejects
v16/v18/20.17/20.18/21.x/22.11. (Dropped an npm-absent sub-case that can't be
isolated in-harness — command -v npm still finds the runner's real npm.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make `update.sh` fetch the CI-built release tarball by default instead of
rebuilding the SPA on the server, so a normal update needs no Node/npm on the
box. This keeps updates working on old distros, ARM/Pi and tiny hosts where the
vite 8 toolchain (Node 20.19+/22.12+) can't or shouldn't run — the Node
requirement stays a build-time/CI concern, not a runtime one.
On-box source rebuilds remain available and explicit: --source / --main (main
branch), --branch <name>, or BUILD_FROM_SOURCE=1. --branch now implies source
mode (clears the release default). npm is only required for those paths.
Test: test_update.sh +1 (mode mapping — default=release, --source/--branch/
BUILD_FROM_SOURCE=source). 49 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slack pulled its brand from Simple Icons, so react-icons removed SiSlack (only
SiSlackware, the Linux distro, remains). Under vite 8 / rolldown this is a hard
build error (MISSING_EXPORT) rather than a silent no-op. Swap the Connections-hub
Slack glyph to Font Awesome's stable FaSlack (same react-icons/fa pattern already
used for route53's FaAws). Verified with a vite 8 production build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The vite 8 upgrade (dcf05d1) makes the frontend build require Node ^20.19.0 ||
>=22.12.0 — on anything older npm silently skips rolldown's native binary and
`npm run build` dies with a cryptic MODULE_NOT_FOUND. Servers that build the SPA
on-box (source install + every update) would break their next update. So:
- install.sh node_ready() now gates on the real vite 8 floor (was Node >=18) and
provisions NodeSource 22 LTS when the host Node is too old.
- update.sh gains _node_build_ok(): build_frontend_bundle refuses (returns 1 →
caller rolls back) and the Docker-host rebuild halts, both with an actionable
"upgrade Node" message instead of a rolldown stack trace.
- Dockerfile + CI (release, release-smoke, extensions, scripts-nightly) bumped
node 20 -> 22 so the shipped/pre-built bundle is built on a compatible Node.
Also fixes a pre-existing parity bug the operator caught: update.sh rendered the
systemd unit WITHOUT substituting @BIND_HOST@ (install.sh does), so an update
left the literal placeholder and gunicorn would fail to bind. update.sh now
substitutes it (loopback default, SERVERKIT_BIND_HOST override) like install.sh.
Tests: test_update.sh +3 (node-floor gate, build refusal on old Node,
@BIND_HOST@ substitution) — 48 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two gaps for existing installs:
- The tint ran only in install.sh, so `serverkit update` (which has its own
build path in scripts/update.sh) would rebuild with the DEFAULT color and a
fixed favicon.ico — existing installs would never get a unique mark.
- Re-tinting on every rebuild would also repaint the box on each update.
Fix: assign the color ONCE and persist it in /etc/serverkit/favicon-color
(survives updates), then re-apply the SAME color from both install and update.
update.sh applies it to the staged dist unconditionally (after the guarded
build), so it works whether dist was rebuilt (source mode) or shipped prebuilt
(release). An install that predates the feature picks up a color on its first
update and keeps it thereafter — a car keeps its paint.
Verified: bash -n on both scripts; persistence confirmed (re-run reuses the
stored color).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The install-time tint only recolored favicon.svg, but the page also shipped a
fixed raster favicon.ico + .png — and favicon-hash scanners hash /favicon.ico,
which was identical across every install. So the "car-paint" tint didn't change
what those tools cluster on.
- index.html links only the SVG now (drop the raster <link>s).
- Remove the unused favicon-16x16/32x32.png.
- install.sh: after tinting favicon.svg, copy it over favicon.ico too, so a
blind /favicon.ico fetch also returns the per-install color instead of a fixed
icon. (SVG bytes under an .ico name — browsers use the linked SVG; this only
affects what a scanner hashes at that URL.)
Still obscurity/defense-in-depth, not a real gate — but now it does what it says.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues from PR review:
- Login: the AuthLayout wrapper only reached the 2FA and login-link states —
the primary login form still had a hardcoded auth-container/auth-card, so the
`login_layout` setting didn't affect the page most users see. Wrap the main
form in AuthLayout too.
- apps.py: the legacy scalar-port fallback for a BYO-image app omitted `expose`,
which AppPortService defaults to `public` (binds 0.0.0.0), publishing the port
past nginx. Default it to `local` (loopback). (Workers set no port, so this
was latent, but the generic path is now safe.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>