Commit Graph
1315 Commits
Author SHA1 Message Date
Juan DenisandClaude Opus 4.8 bb899522fe docs(analytics): user guide + marketplace recommendations (Phase 7)
- 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>
2026-07-18 02:58:11 -04:00
Juan DenisandClaude Opus 4.8 7bc323b41b feat(analytics): log ingestion + optional geo (Phase 6)
- 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>
2026-07-18 02:44:58 -04:00
Juan DenisandClaude Opus 4.8 d6e8c737dc feat(analytics): WordPress + nginx tracker wiring (Phase 5)
- 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>
2026-07-18 02:38:39 -04:00
Juan DenisandClaude Opus 4.8 0def464703 feat(analytics): dashboard frontend, six tabs (Phase 4)
- AnalyticsPage: route-driven tab shell (Overview/Pages/Referrers/Devices/
  Realtime/Sites), loads the site list, topbar site picker for report tabs,
  zero-sites empty state.
- OverviewTab: KPI tiles + recharts area chart (visitors+pageviews) + top
  pages/referrers DataTables + live 30-min counter.
- PagesTab / ReferrersTab: range picker + sorted DataTable.
- DevicesTab: device/browser/OS/country tables in a responsive grid.
- RealtimeTab: active-visitor + pageview counters, minutes selector, recent-hit
  table, 15s polling.
- SitesTab: site CRUD (Modal add form, ConfirmDialog rotate/delete, enable
  toggle) + tracking-snippet modal (copy + outbound-link opt-in).
- RangePicker + format utils; isolated analytics.scss (CSS custom props +
  neutral rgba, namespaced under .analytics-page).
- Synced into frontend/src/plugins/serverkit-analytics. npm run lint (0 errors)
  and npm run build both green; sync --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 02:25:41 -04:00
Juan DenisandClaude Opus 4.8 b61e1d5c18 feat(analytics): tracker JS + snippet generator (Phase 3)
- backend/tracker/sk.js: real cookieless tracker (~3.1 KB min) — pageview,
  external referrer, Navigation-Timing load ms, screen-class bucket, language;
  SPA nav via history patch + popstate; opt-in outbound/download click tracking;
  honors DNT; navigator.sendBeacon (text/plain, no preflight) with fetch
  keepalive fallback; fails silently.
- scripts/build-analytics-tracker.mjs: dependency-free conservative minifier
  (strip comments, trim, keep newlines) → backend/tracker/sk.min.js, with a
  --check drift gate. Built artifact checked in (3147 bytes, under the 4 KB cap).
- analytics.py: GET /sites/<id>/snippet returns the ready-to-paste <script>
  snippet (site_key + panel tracker URL, optional data-outlinks).
- dev/test-tracker.html: manual smoke page (load tracker, fire SPA nav/outlinks).
- 6 new tests (artifact bounded + comment-free, real build served, snippet +
  outlinks flag + 404). 59 total.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 02:05:29 -04:00
Juan DenisandClaude Opus 4.8 e0cde1d93e feat(analytics): rollups + report API (Phase 2)
- 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>
2026-07-18 02:00:45 -04:00
Juan DenisandClaude Opus 4.8 713e0e811c feat(analytics): data model + public collector (Phase 1)
- models.py: 4 tables (site/event/daily/log_cursor), all
  ext_serverkit_analytics_* prefixed; site_key generator; JSON settings blob.
- ingest_service.py: cookieless visitor hashing (daily-rotating in-memory HMAC
  salt, never persisted, raw IP/UA discarded), UA bot filter + lightweight
  browser/os/device parse, in-process token-bucket rate limiter (per site_key +
  per IP), buffered batch inserts with a daemon flush thread (5s / buffer_max,
  skipped under TESTING), payload validation + event build.
- analytics.py: public POST /collect (site_key auth, 8 KB body cap, per-site
  origin allowlist + reflected CORS, DNT + bot drop as silent 204, 429 on rate
  limit) and GET /tracker.js (placeholder; real build Phase 3). Client IP via
  the plan-48 trusted-proxy helper.
- rollup_service.py: retention prune (raw events + old rollups); rollup stub.
- 39 proving tests (models/purge, bot/UA/hash units, collector happy path +
  404/403/400/413/429, DNT + per-site override, origin allowlist, CORS reflect,
  preflight, query-string gate, retention prune).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 01:55:02 -04:00
Juan DenisandClaude Opus 4.8 cc3626267a feat(analytics): scaffold serverkit-analytics extension (Phase 0)
Native, privacy-first web-analytics builtin extension. Phase 0 lands the
manifest, backend skeleton, and pre-bundled frontend placeholder:

- plugin.json: entry_point analytics:analytics_bp, /api/v1/analytics prefix,
  models/jobs/schedules/config_schema/lifecycle, operations-nav contribution,
  permissions db/network/filesystem.
- backend: analytics_bp (public /ping), config accessor (DEFAULTS + get_cfg),
  lifecycle (seed config defaults, upgrade backfill, injection cleanup on
  uninstall), job handler stubs (rollup/retention_prune/log_tail) fleshed out
  in later phases.
- frontend: AnalyticsPage placeholder, synced into frontend/src/plugins.
- tests: 14 proving tests (manifest validates, refs resolve, config accessor,
  lifecycle seeding, ping route).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 01:46:15 -04:00
Juan DenisandClaude Opus 4.8 b298c78d28 test(security): bump test-count ratchet for plan 48 proving tests
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>
2026-07-18 01:25:38 -04:00
Juan Denis 5fd04bbfb0 Add review rules and proxy/auth docs
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.
2026-07-18 01:23:47 -04:00
Juan DenisandClaude Opus 4.8 35f0abc456 feat(auth): per-IP login brute-force throttle
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>
2026-07-18 00:59:19 -04:00
Juan DenisandClaude Opus 4.8 363a09dc7e feat(security): trust reverse-proxy client IP via one gated seam
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>
2026-07-18 00:51:21 -04:00
Juan Denis 9cc00bbf2b Revise PR title rules for human-readable names
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.
2026-07-17 19:41:03 -04:00
github-actions[bot] 85b012c7eb chore: bump version to 1.7.40 [skip ci] 2026-07-16 02:34:58 +00:00
Juan Denis 7688db181c Refresh ServerKit logo SVG
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.
2026-07-15 22:34:43 -04:00
Juan Denis b98265a3d7 Move PageTitleUpdater inside AuthProvider
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.
2026-07-15 22:05:19 -04:00
github-actions[bot] a630096835 chore: bump version to 1.7.39 [skip ci] 2026-07-16 00:47:26 +00:00
Juan Denis 33a90d7b2b Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-07-15 20:47:30 -04:00
Juan DenisandClaude Opus 4.8 df1b85055d test(install): update node_ready test to the vite 8 Node floor
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>
2026-07-15 20:40:18 -04:00
github-actions[bot] 785015caf7 chore: bump version to 1.7.38 [skip ci] 2026-07-16 00:33:13 +00:00
Juan DenisandClaude Opus 4.8 309851dabf feat(update): default to pre-built release updates (Node-free server)
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>
2026-07-15 20:32:17 -04:00
Juan DenisandClaude Opus 4.8 dc1d8eeda1 fix(icons): replace removed SiSlack with FaSlack (react-icons dropped Slack)
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>
2026-07-15 20:20:41 -04:00
Juan DenisandClaude Opus 4.8 ca354e228a harden(installer): raise Node floor to 20.19+/22.12+ for vite 8 + fix update.sh @BIND_HOST@
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>
2026-07-15 20:17:48 -04:00
Juan Denis dcf05d1cc1 Merge vite 8 upgrade (vite 5→8 + rolldown manualChunks, @vitejs/plugin-react 4→6) 2026-07-15 20:03:01 -04:00
Juan Denis b7911026be Merge plan-47 lean-core (wizard installs + 4 backend extractions + security tab-group) 2026-07-15 15:24:07 -04:00
github-actions[bot] 514873f230 chore: bump version to 1.7.37 [skip ci] 2026-07-15 18:20:14 +00:00
Juan Denis ec35690582 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-07-15 14:19:25 -04:00
Juan DenisandClaude Opus 4.8 56c1ab6a27 fix(favicon): persist the per-install tint + apply it on update too
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>
2026-07-15 14:08:26 -04:00
Juan DenisandClaude Opus 4.8 8ee33ef2c6 fix(favicon): make the per-install tint actually cover the fingerprint URLs
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>
2026-07-15 14:04:00 -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
github-actions[bot] d7af18a7a9 chore: bump version to 1.7.36 [skip ci] 2026-07-15 17:19:38 +00:00
Juan DenisandClaude Opus 4.8 be0882ca24 chore(frontend): reconcile package-lock with package.json
The lockfile had drifted from package.json (which is why `tramo` — a declared
dep — was never installed and `vite build` failed to resolve `tramo/react`).
`npm install` reconciled node_modules; commit the resulting lockfile so `npm ci`
installs are reproducible and the frontend builds cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 13:17:25 -04:00
Juan DenisandClaude Opus 4.8 45b6f8bc52 refine: per-install favicon uses a continuous shade, not a fixed palette
A fixed list of N colors means two installs collide on the identical hex. Use
the "car-paint" model instead: a curated base hue (tasteful families, no muddy
yellows) + a few degrees of jitter + randomized saturation/lightness (rich,
mid-dark — never washed-out). Same family can recur, but the exact shade rarely
does, so favicon-hash comparison across installs almost always differs. Emitted
as an hsl() fill (SVG renders it natively; still a pure text replace).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 13:14:07 -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 70027b9fea Revert panel de-branding — keep the ServerKit identity, keep only noindex
Restore the ServerKit name, favicon, manifest, auth-page headings, and
service-worker strings. Genericizing the panel's identity was the wrong
trade: hiding the title/favicon is security-by-obscurity that a real scanner
sees through, and it erased the product identity for almost no benefit. The
pages it touched (login/setup/register) require server or admin access to
reach anyway, so there is nothing to hide there.

The one durable, no-cost measure is kept: the `X-Robots-Tag: noindex`
header (from the prior commit) stays on both nginx server blocks so the
panel isn't indexed by search engines. Real protection lives in access
control + auth, not in the favicon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 12:40:21 -04:00
Juan DenisandClaude Opus 4.8 7c38e44ba0 chore: drop orphaned old favicon assets under src/assets/favicon
These raster favicons (favicon.ico/.png) were unreferenced dead files — the
app only ever served /favicon.* from public/. Removing them completes the
favicon cleanup from the de-fingerprint change; the served icon is the
generic public/favicon.svg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 12:36:29 -04:00
Juan DenisandClaude Opus 4.8 70d445da32 harden: de-fingerprint the panel + keep it out of search indexes
Reduce how easily the panel can be enumerated by scanners (Shodan/Censys)
and search engines:

- nginx: emit `X-Robots-Tag: noindex, nofollow, noarchive` on the panel
  server blocks (both the TLS and insecure fallback configs).
- Generic page identity: title "Control Panel" + neutral meta description
  in index.html; the runtime document.title setters (App.jsx, usePageTitle)
  and PWA manifest name/short_name match — so the browser tab and manifest
  no longer say "ServerKit".
- Drop the distinctive favicon (.ico/.png removed, generic favicon.svg) so a
  favicon-hash no longer clusters instances; strip the product name from the
  service worker (cache name, push title, header comment).
- Neutralize the visible auth/splash copy (Login/Register/Setup headings,
  AppLoader label) to a generic name.

Search engines are the easy case; the header is defense-in-depth. Real
hiding still requires access control (IP allowlist / VPN / zero-trust) and
is deployment-specific.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 12:32:15 -04:00
Juan DenisandClaude Opus 4.8 69123a8b80 harden: broaden insecure-secret detection, bind API to loopback, fix break-glass sub
- config: is_insecure_secret() rejects any dev/placeholder key (dev-*,
  jwt-secret-key*, *change-in-production*, *change-this*, empty), not just the
  two exact default strings — a variant like `dev-jwt-...` can no longer boot
  in production. Used in both ProductionConfig.__init__ and init_app.
- systemd unit: bind gunicorn to 127.0.0.1 by default so the raw API port is
  not world-reachable (host nginx already fronts :80/:443). Override with
  SERVERKIT_BIND_HOST=0.0.0.0 for setups that front it differently.
- cli break-glass: mint the token with a string identity; flask-jwt-extended
  rejects a non-string `sub` at decode time with a 422, which broke every CLI
  call that authenticated against the panel API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 12:07:14 -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 176141295f docs(plan47): sync extension manifests, raise ratchet, changelog + docs
Plan 47 Phase 6 (for shipped work). Sync the pre-bundled frontend plugin.json
copies for the four extracted builtins (entry_point/url_prefix + status
description) so builtin-extensions/ and frontend/src/plugins/ agree
(sync-builtin-frontends --check + test_extension_manifests green). Raise the
test-count ratchet 2588 -> 2623. Document the second-speed backend-extraction
recipe in EXTENSIONS.md, add CHANGELOG entries (wizard installs + opt-in
FTP/cloud/remote-access/status), and a README "lean by default" bullet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 23:05:06 -04:00
Juan DenisandClaude Opus 4.8 a5e19ce6bf feat(security): open the Security tab group to extension contributions
Plan 47 Phase 3a. SECURITY_TABS' TabGroupLayout now passes groupId="security"
and mounts {groupRoutes.security}, so extracted security features (fail2ban,
clamav, …) can contribute tabs + group-nested routes via their manifests and
return as tabs in the exact place users know — the same mechanism the
files/servers/monitoring groups already use. Inert until a security extension
ships; no behavior change on its own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:38:35 -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
github-actions[bot] b2ada5bcae chore: bump version to 1.7.35 [skip ci] 2026-07-13 01:59:49 +00:00
Juan DenisandClaude Fable 5 783b0876bd Build the frontend stage natively in multi-arch Docker builds
The release workflow builds linux/amd64,linux/arm64 via buildx + QEMU on
an amd64 runner, so the frontend-builder stage ran `npm ci`/`npm run
build` under emulation for arm64 -- 20-40 minutes when it did not hang
outright. Its output (frontend/dist) is static, arch-independent assets,
so pin the stage to $BUILDPLATFORM: buildx runs it natively, once, shared
by both platform images. Final images are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:59:37 -04:00
github-actions[bot] 09f3a2312d chore: bump version to 1.7.34 [skip ci] 2026-07-13 01:42:36 +00:00