Commit Graph
1334 Commits
Author SHA1 Message Date
Juan Denis b4ff7a07d5 Update login and WordPress screenshots
Refresh the documentation screenshots for the login and WordPress screens to reflect the current UI.
2026-07-20 01:29:14 -04:00
Juan DenisandClaude Opus 4.8 f8ff93a4bd docs(readme): accept crypto donations
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>
2026-07-20 01:19:54 -04:00
Juan DenisandClaude Opus 4.8 4f08ae47a9 fix(docs): commit the architecture diagrams that .gitignore swallowed
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>
2026-07-20 01:19:30 -04:00
Juan DenisandClaude Opus 4.8 a83dbd3e37 docs(readme): refresh the English README and rebuild all three translations
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>
2026-07-20 01:17:53 -04:00
Juan DenisandClaude Opus 4.8 c6271da13e docs(architecture): rewrite against the current code, add diagrams
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>
2026-07-20 01:17:06 -04:00
Juan DenisandClaude Opus 4.8 adf70b4df0 docs: correct stale architecture facts in the agent instructions
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>
2026-07-20 01:17:06 -04:00
Juan DenisandClaude Opus 4.8 d595ea14ff fix(docs): remove the broken gunicorn worker class from the install guide
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>
2026-07-20 01:16:52 -04:00
Juan DenisandClaude Opus 4.8 9156b88097 docs: add the missing MIT LICENSE file
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>
2026-07-20 01:16:52 -04:00
Juan DenisandClaude Opus 4.8 f4c93e8fe3 docs(roadmap): mark the core feature-complete
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>
2026-07-20 01:16:52 -04:00
Juan DenisandClaude Opus 4.8 7d22c2cbcf feat(ui): move env var creation into a modal, always store as secret
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>
2026-07-20 01:16:39 -04:00
Juan DenisandClaude Opus 4.8 bc3711f32d fix(ui): stop detail-page tabs jumping, unify them as segmented bars
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>
2026-07-20 01:16:39 -04:00
Juan DenisandClaude Opus 4.8 a6867c8757 feat(scripts): scaffold sub-path routes in new-extension
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>
2026-07-20 01:16:25 -04:00
Juan DenisandClaude Opus 4.8 0e7bf511f6 feat(extensions): add serverkit-localkit builtin
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>
2026-07-20 01:16:25 -04:00
Juan DenisandClaude Opus 4.8 9639288494 refactor(extensions): extract seven builtins to standalone repos
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>
2026-07-20 01:16:16 -04:00
Juan DenisandClaude Opus 4.8 8a9aa03d8f chore(frontend): drop unused eslint-disable in capture-skeletons script
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 19:23:59 -04:00
Juan DenisandClaude Opus 4.8 3333ede87b feat(frontend): runtime self-captured skeleton bones (plan 50 phase 3)
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>
2026-07-18 19:20:19 -04:00
Juan DenisandClaude Opus 4.8 67142efd13 feat(frontend): dev-time skeleton bone capture + bones replay (plan 50 phase 2)
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>
2026-07-18 19:15:46 -04:00
Juan DenisandClaude Opus 4.8 c687ea4dc4 feat(frontend): consolidate skeletons + overlay SkeletonBoundary (plan 50 phase 1)
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>
2026-07-18 19:11:55 -04:00
Juan DenisandClaude Opus 4.8 d1589024e3 test(analytics): bump test-count ratchet for the analytics extension
Full backend suite green (2669 passed, 53 skipped). serverkit-analytics adds 83
proving tests (+ 2 parametrized manifest-hygiene cases). Ratchet 2637 -> 2722.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 03:15:06 -04:00
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