1103 Commits
Author SHA1 Message Date
Juan Denis 0689dc7578 Merge pull request #57 from jhd3197/dev
Stop extension pages ghost-rendering and revive live WebSocket updates
v1.7.11
2026-07-03 03:21:21 -04:00
github-actions[bot] 2fab4ac108 chore: bump version to 1.7.11 [skip ci] 2026-07-03 04:17:11 +00:00
Juan Denis a63b75f162 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-07-03 00:16:56 -04:00
Juan DenisandClaude Fable 5 b2b9f58016 docs(changelog): record the ghost-render and WebSocket worker fixes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:15:13 -04:00
Juan DenisandClaude Fable 5 751dde7a7d fix(deploy): threaded gunicorn worker — gevent-websocket class broke WebSocket
The app runs flask-socketio with async_mode='threading' (the backend
is real-thread based: jobs scheduler, queue bus, agent registry), where
engineio serves WebSocket via simple-websocket on the raw socket. The
systemd unit template and Dockerfile still launched gunicorn with the
GeventWebSocketWorker, which ALSO answers the upgrade — the client got
HTTP/1.1 101 twice and read the second one as a frame ('Invalid frame
header'), so every panel silently degraded to polling. Proven by
hexdump (double 101, first 'frame' byte 0x48 'H') and fixed live:
single 101 + 0x81 text frame, socket.io connects through Cloudflare.

- unit template + Dockerfile: gunicorn -w 1 --threads 100 (single
  PROCESS stays mandatory — in-memory agent gateway)
- requirements: pin simple-websocket explicitly (was transitive-only)
- CLAUDE.md / ARCHITECTURE.md / SECURITY.md single-worker notes updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:14:49 -04:00
Juan DenisandClaude Fable 5 865e451c89 fix(plugins): stop legacy auto-render from mounting extension pages globally
PluginLoader's legacy auto-render (any plugin index default export
renders globally) only excluded plugins that declared a WIDGET
contribution. The WordPress and Cloudflare-ops builtins declare routes
+ nav but no widgets, and their index modules had surplus default
exports — so their PAGES mounted globally on every route: the CF zone
page ran with zoneId=undefined (zones/undefined/settings 404s), the
WordPress sub-router swallowed the current location as a site id
(/domains → sites/domains 404s), and visited pages stacked into one
view ('5 pages in one'). Reproduced live via Playwright.

- legacy auto-render now skips any plugin declaring ANY contribution
  (routes, nav, tabs, widgets, palette, layouts, ai) — those own their
  rendering; serverkit-gui keeps working via its declared widget
- surplus default exports removed from both builtins (synced copies +
  builtin-extensions sources) with do-not-re-add comments; routes
  resolve named exports

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:07:06 -04:00
Juan DenisandClaude Fable 5 4af3df1136 fix(nginx): never-cache the SPA entrypoint; warn on stale :3847 panel vhosts
A test box's panel domain kept serving a Jun 25 bundle after the Jul 3
update — the domain's custom vhost still proxied the retired :3847
frontend container, and the old bundle's calls to deleted legacy
marketplace endpoints read as 'Failed to load extensions'. It looked
exactly like a browser/CDN cache problem (it wasn't — cf-cache-status
was DYNAMIC; the origin itself was stale).

- both stock confs: expires -1 on the SPA entrypoint (emits
  Cache-Control: no-cache WITHOUT discarding server-level security
  headers, unlike add_header-in-location) — a cached index.html
  references hashed assets that vanish on the next blue/green switch
- update.sh refresh_config now runs report_stale_panel_vhosts: warns,
  naming the file, when any enabled vhost still proxies :3847;
  observation-only per R1, silent on a missing sites-enabled dir,
  added to the fresh-box loop per policy
- proving tests: entrypoint no-cache asserted for both confs; stale
  vhost flagged (and clean one not); missing-dir silence (42 passed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:48:15 -04:00
Juan Denis 19f009c9fd Merge pull request #56 from jhd3197/dev
Errexit-hardening sweep, plus honest version and install-dir resolution
v1.7.10
2026-07-02 22:52:55 -04:00
github-actions[bot] c301224844 chore: bump version to 1.7.10 [skip ci] 2026-07-03 02:39:24 +00:00
Juan DenisandClaude Fable 5 e93faac226 feat(servers): store the agent's self-reported footprint dirs
The agent (serverkit-agent bbaf4e2) now sends install_dir and
config_dir in system_info. Panel side:

- servers.agent_install_dir + agent_config_dir columns (migration 047,
  idempotent guard like its siblings) with the same coalesce contract
  as the other system_info fields — a transient payload gap never
  wipes a stored value
- exposed in Server.to_dict and /servers/available; TargetPicker
  passes them on the agent target
- the File Manager rail prefers the reported config dir over the
  installer convention (backslash-normalized for Windows agents;
  agent-logs link derives <config_dir>/logs)
- proving test: full system_info -> DB -> available round trip +
  coalesce survival

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:36:34 -04:00
Juan Denis e46630bb3f Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-07-02 22:31:17 -04:00
Juan DenisandClaude Fable 5 9dfed262d6 feat(files): per-target quick-access rail for remote agents
Browsing a remote agent no longer shows the panel host's shortcuts:

- /servers/available now returns each server's os_type
  (linux/windows/darwin, null for pre-sysinfo agents) and TargetPicker
  passes it through on the agent target
- the File Manager rail switches per target: Linux agents link the
  agent's own footprint (/etc/serverkit-agent, fixed by the served
  installer) plus the generic Unix paths; Windows agents get
  ProgramData/ServerKit/Agent, its logs, and C:/Users instead of Unix
  paths that don't exist; darwin gets /Users + /var/log; unknown
  os_type falls back to the Linux set (the historical behavior)
- local target unchanged: panel set with the resolved install dir
- 2 proving tests for the os_type contract (present + null for legacy)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:26:46 -04:00
Juan DenisandClaude Fable 5 c163d4357f feat(files): Stack quick link follows the real install dir
/system/version now returns install_dir (new get_install_dir() in the
shared version resolver: SERVERKIT_INSTALL_DIR override -> own tree ->
/opt/serverkit default; abspath not realpath so blue/green boxes report
the stable symlink). The File Manager re-points its 'Stack' quick-access
shortcut from the response, so custom-directory installs jump to the
real tree; the static default remains the fallback while loading or on
older backends. 2 new resolver tests + install_dir asserted in the
endpoint test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:19:14 -04:00
Juan DenisandClaude Fable 5 e797ec3a3f fix(system): version resolution honors custom install dirs and Docker
The About page's /system/version (and check-update's current-version
read) hardcoded /opt/serverkit first, duplicated in three places:

- app/utils/version.py is now the single resolver: explicit
  SERVERKIT_INSTALL_DIR override -> the running code's own tree ->
  /opt/serverkit last (a box moved to a custom dir may still have a
  stale tree there); system.py's two inline copies now call it
- the systemd unit template renders SERVERKIT_INSTALL_DIR from the
  installer's SERVERKIT_DIR (deliberately not the bare SERVERKIT_DIR
  env name, which app/paths.py already uses for the /var/serverkit
  data root); flows to existing installs via the updater's re-render
- crypto's .env discovery gains the same install-dir hint
- Docker image now ships VERSION (/app/VERSION) — containers showed
  the 1.0.0 fallback because no candidate path existed in the image
- 4 proving tests in backend/tests/test_panel_version.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:14:37 -04:00
github-actions[bot] 180df2c29d chore: bump version to 1.7.9 [skip ci] 2026-07-03 02:09:44 +00:00
Juan Denis b57f904154 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-07-02 22:03:04 -04:00
Juan DenisandClaude Fable 5 21c16f9d6e docs(scripts): add the tested-context set -e suppression rule to the PR checklist
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:53:52 -04:00
Juan DenisandClaude Fable 5 7345807b12 test(scripts): chain multi-statement if-subshell assertions (no vacuous passes)
bash suppresses set -e inside any tested-context command list — an
if-condition subshell, or the left side of ||, even with set -Eeuo
pipefail re-armed inside — so multi-statement test bodies only gated
on their LAST statement. Today's cmp-less EL9 CI run proved it: a
byte-identity test passed while its cmp assertions exited 127.

28 blocks across test_update/test_install/test_lib/test_agent_install
converted to explicit && chains so every assertion gates the result.
No test weakened; no vacuous pass was hiding a real script bug (each
newly-gated statement was verified to genuinely succeed).

Verified: Git Bash 171 passed / 0 failed; rockylinux:9 all five
suites 177p/0f; opensuse/leap:15.5 148p/0f (pre-existing python3/tar
skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:53:14 -04:00
Juan DenisandClaude Fable 5 b44305d4f7 test(install): survive minimal CI images without cmp/tar; chain if-subshell assertions
EL9/Fedora containers ship without diffutils and openSUSE Leap without
tar, which failed 4 of the 7 cross-distro unit jobs:

- files_identical() in stubs.sh: cmp when available, sha256sum
  (coreutils, everywhere) otherwise
- the two tarball-fixture tests skip cleanly when tar is absent
  (harness's existing skip convention; proven on the other distros)
- the t16/t16r assertion lists are now && chains: bash suppresses
  set -e inside an if-condition subshell, so sequential statements
  only gated on the LAST one — the cmp-less EL9 run proved it by
  passing t16r vacuously while cmp exited 127

Verified in the actual images: rockylinux:9 65p/0f/0s,
opensuse/leap:15.5 61p/0f/4s (was 63p/2f and 61p/2f).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:38:37 -04:00
github-actions[bot] 1296a5c826 chore: bump version to 1.7.8 [skip ci] 2026-07-03 01:31:06 +00:00
Juan DenisandClaude Fable 5 90d252fbad docs(changelog): record the scripts-reliability round-2 hardening
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:48:36 -04:00
Juan DenisandClaude Fable 5 fbea0f4737 docs(scripts): PR checklist for shell changes + updater channel decision
- PULL_REQUEST_TEMPLATE gains a scripts-only checklist condensing the
  failure-semantics rules (guarded observations, no &&-tail returns,
  capture-safe stdout, errexit-immune helpers, fresh-box loop policy,
  proving test in the same commit)
- record the self-update channel decision in the bootstrap header:
  keep fetching from main, gated by the update-e2e CI job, preserving
  one-push-fixes-the-fleet

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:44:07 -04:00
Juan DenisandClaude Fable 5 4874fa9161 ci(scripts): real install+update e2e gate, nightly tarball/from-release runs
The July 2 outage lived in update.sh's top-level run block — code the
unit matrix never executes. Close the blind spot:

- update-e2e (PR-gated): real install.sh on the runner VM, health
  probe, then THIS branch's update.sh --force with self-update
  disabled, re-probe; failure dumps /var/log/serverkit + journal
- scripts-nightly.yml: install from the built release tarball (proves
  the offline path + shipped-venv relocation), and install latest
  published release -> update to current checkout (carry-forward
  regressions same-version --force runs can't catch)
- advisory (non-gating) shellcheck --enable=all on install.sh/update.sh
- unit steps now also run test_cli.sh + test_agent_install.sh in both
  the ubuntu job and the 7-distro matrix

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:42:57 -04:00
Juan DenisandClaude Fable 5 502ed19ae0 test(scripts): fresh-box observation loop, failing-stub library, exit-status assertions
The July 2 outage function had a unit test — with a populated fixture
only. This encodes the missing net:

- fresh-minimal-box fixture loop: every observation/discovery/snapshot/
  report function in update.sh (17) and install.sh (16) must survive a
  zero-app box under set -Eeuo pipefail; policy comment requires new
  functions to be appended
- scripts/test/stubs.sh: shared failing-stub factories (systemctl exit
  5, docker fail/empty, curl exit 22, GitHub rate-limit JSON)
- assertion sweep: 43 bare capture-then-compare sites now include exit
  status in the assertion
- best-effort contract test: the run block's real APP_BASELINE ->
  report chain exits 0 on a fresh box AND on a genuine regression
- fix(update): the loop immediately caught report_failure's trailing
  '[ -n $UPDATE_LOG ] && printf' returning 1 when UPDATE_LOG is empty
  (same species as the outage) — converted to if, added to the loop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:30:26 -04:00
Juan DenisandClaude Fable 5 11066bb130 fix(agent-install): paged version discovery, checksum verify, busybox support (A1-A3)
- version discovery pages agent-v tags (?per_page=100, 3 pages) so
  panel releases can no longer push agent tags off page 1; panel
  injects its resolved agent version into the served script and its
  own releases fetch is paginated too (A1)
- downloaded agent binary is sha256-verified against the release's
  checksums.txt: halt on mismatch, warn-and-continue when absent (A2)
- value-taking options error loudly when the value is missing; busybox
  adduser fallback; no GNU-only grep -oP; pipe-safe source guard (A3)
- test-routing.sh fixed (PASS++ suicide under set -e, guarded probes,
  curl 000000 false-pass) rather than deleted - still live tooling
- new scripts/test/test_agent_install.sh (18 passed); backend server
  tests 110 passed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:00:24 -04:00
Juan DenisandClaude Fable 5 62300b4299 fix(cli): guard start/restart/logs/add-site, loud --branch error, RHEL nginx layout (L4-L7,L9,L12)
- update --branch without a value errors loudly instead of dying
  silently on shift (L5)
- cmd_start/cmd_restart mirror cmd_stop's systemctl/docker guards (L6)
- cmd_logs degrades past a dead/missing journalctl to docker logs (L7)
- cmd_add_site/remove_site/list_sites detect Debian vs conf.d nginx
  layout; reloads are guarded with a manual-reload hint (L9)
- uninstall on a remnants-only box prints exact recovery commands (L4)
- portable port extraction (no grep -oP), read-at-EOF defaults,
  compare_versions rejects unparsable as update-available, doctor
  exits nonzero on issues (L12); BASH_SOURCE guard for testability
- new scripts/test/test_cli.sh (22 passed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:00:15 -04:00
Juan DenisandClaude Fable 5 ccfd9ed227 fix(lib,uninstall): warn-and-no-op state ops, best-effort teardown, remnants fallback (L1-L4,L8,L10-L13)
- state.sh mutating ops genuinely warn-and-no-op on OSError; reads
  quiet-empty; save refuses to clobber unread state (L1)
- uninstall lib: guarded state_list pipeline (L2), _u_try wrapper so
  rm/mkdir failures never abort mid-teardown (L3), telemetry respects
  dry-run + --max-time 5 (L8)
- root uninstall.sh: inline best-effort fallback teardown when the lib
  is missing — remnants-only boxes can now be uninstalled (L4)
- pkg_refresh matches its never-propagate docstring (L10); ufw detect
  capture-then-case (L11); init/firewall zero-arg safety, init cache
  actually used, init_reload contract documented (L13)
- 18 new proving assertions in test_lib.sh (31 passed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:59:55 -04:00
Juan DenisandClaude Fable 5 8a2d2ffd83 fix(install): guard host probes, docker bootstrap, swap, release fallback (I11,I13-I19,I22-I24)
- EUID check runs before any host write; friendly non-root halt (I11)
- get.docker.com staged to temp file with retry + pkg fallback (I13)
- df -Pk on the real install target; free guarded for LXC (I14)
- release tag via releases/latest redirect (no API quota) + loud
  source-fallback warning (I15)
- tar/cp failures in fetch_release restore live state and fall back
  to source cleanly (I16)
- svc_enable/svc_start init-system abstraction at service call sites (I17)
- stale slot-A collision guard in ensure_install_layout (I18)
- swap verified via /proc/swaps before claiming active; dd/mkswap
  failures degrade to warn (I19)
- VERSION clobber, telemetry pipefail, stale SSL_MODE rewrite,
  apply_frontend_root end-of-loop && (I22-I24, bonus)
- 22 new proving assertions (61 passed local, 64 on Linux)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:33:58 -04:00
Juan DenisandClaude Fable 5 f2e7715479 fix(install): close data-loss re-run + no-domain abort, harden pkg/venv/docker paths (I1-I10,I12,I20,I21)
- no-domain curl-pipe install no longer aborts at the nginx phase (I1);
  blank Enter at the domain prompt no longer aborts interactive installs (I2)
- re-running install.sh over a live install preserves .env and
  backend/instance/ (stash/restore, fixed backend/app detection) (I4, I20)
- symlinked default layout no longer deletes its own venv copy source (I3)
- pkg_add/refresh_pkg_index genuinely warn-and-continue under set -e;
  yum arm drops dnf-only --refresh (I6, I12)
- restore Rocky/RHEL-9 paired openssh+openssl upfront upgrade (I5)
- git installed on demand for source sync (I7); locate_python validates
  venv capability + installs pythonX.Y-venv on Debian-family (I8)
- compose plugin fallback chain docker-compose-plugin -> -v2 -> warn (I9)
- dnf5 config-manager addrepo fallback for Fedora 41+ (I10)
- firewall-phase state writes are best-effort (I21)
- 25 new proving assertions in test_install.sh (40 passed local, 42 on Linux)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:13:04 -04:00
Juan DenisandClaude Fable 5 4af9e72ad9 fix(update): harden failure semantics — prune, capture purity, rollback re-entry (U1-U14)
- find-based backup pruning; empty/missing BACKUP_DIR is a no-op (U1)
- download_release progress to stderr; stdout is the tarball path only (U2)
- guard bare systemctl stop/start/daemon-reload in forward+rollback paths (U3,U4,U8,U13)
- ROLLING_BACK=1 inside rollback(); never runs twice via the EXIT trap (U5)
- npm preflight for source updates; guarded frontend build pipeline (U6)
- empty-ORIG_ARGS re-exec guard, cleanup_on_exit return 0, df -Pk,
  SIGPIPE guards, cd leaks subshelled, local typo (U7,U9-U12,U14)
- 14 new proving assertions in test_update.sh (35 passed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:50:53 -04:00
Juan Denis a90c56428f Make update checks tolerant and add empty-dir test
Allow failures when querying GitHub release tag (prevent pipeline from aborting) and add a unit test ensuring discover_app_upstreams tolerates an empty app-locations directory.

Changes:
- scripts/update.sh: make the curl|grep pipeline resilient by appending `|| true` so the command substitution won't fail the script when the GitHub API call returns nothing.
- scripts/test/test_update.sh: add test T16b to verify discover_app_upstreams does not abort under `set -euo pipefail` when the app-locations directory is empty.

These changes prevent hard failures during release detection and add coverage for empty-directory handling.
2026-07-02 15:08:34 -04:00
Juan Denis 0e516defcd Avoid failing when no upstreams found in update script
Suppress a non-zero exit from the pipeline in discover_app_upstreams by appending `|| true` after the sort. This prevents the script from exiting with an error when no matching proxy_pass entries are found in APP_LOCATIONS_DIR (e.g., empty or missing files). Improves robustness of scripts/update.sh in scenarios with no app upstreams configured.
2026-07-02 15:02:24 -04:00
Juan Denis c500d01e3b Merge pull request #55 from jhd3197/dev
A real extension platform — remote registry, bundled builtins, and update-survival
v1.7.7
2026-07-02 02:52:16 -04:00
github-actions[bot] 59c9227916 chore: bump version to 1.7.7 [skip ci] 2026-07-02 06:38:07 +00:00
Juan DenisandClaude Fable 5 0ec55accbe feat(extensions): settings.section slot + manifest shape lint (#52)
Extensions can now contribute Settings sections: a settings.section
PluginSlot renders below the active Settings tab with the tab id as
context. _validate_manifest grew shape checks for everything the
platform consumes declaratively — module:attr refs (entry_point,
socket_entry, models, lifecycle hooks, job handlers), jobs/schedules
shapes, and per-kind required contribution keys (nav/routes/tabs/
palette/widgets/layouts) — so a malformed block fails the install with
a message naming each problem instead of being silently dropped at
runtime. Unknown contribution kinds only warn (forward compat). The
scaffolder gained --validate <path> mirroring the same rules for
authors, wired into the conversion recipe in docs/EXTENSIONS.md.

3 new lint tests incl. one asserting every shipped builtin manifest
passes; full backend suite 1227 green; lint 0 errors; build clean;
--validate smoke-tested on a good and a bad manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 02:36:46 -04:00
Juan DenisandClaude Fable 5 136f8b195a refactor(marketplace): retire the legacy Extension catalog (#51)
The DB-seeded Extension/ExtensionInstall catalog was dead weight:
nothing ever populated it on a real panel, so it fed Browse an empty
third lane redundant with the real sources (builtin folder scan, remote
registry, InstalledPlugin state), and its orphaned per-user config
plumbing connected to nothing a plugin could read (superseded by #49).

Removed: the two models, marketplace_service.py, all legacy /marketplace
CRUD/install/rate endpoints (the blueprint keeps only the registry
endpoints), the frontend published lane (catalog mappers, install/
uninstall handlers, rating stars, published stats) and its API methods.
Migration 046 drops the orphaned tables.

Pipeline + registry suites 29 green; lint 0 errors; build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 02:21:45 -04:00
Juan DenisandClaude Fable 5 046ae0eacc feat(extensions): daily scheduled update check + admin notification (#50)
New builtin.extension_updates ScheduledJob (daily) runs
check_for_updates() against the registry and notifies admins via the
Notifications Bus (new extensions.updates_available catalog event,
to=admins). Fingerprint marker in settings de-dups: one notification
per new release set, not one per day; the Marketplace badge stays the
live surface. Dogfoods both the Jobs and Notify extension-facing SDKs.

3 new tests (notify-once-per-release, quiet when none, schedule
seeded); jobs suite builtin count updated 9 -> 10; pipeline+jobs 51
green, notification bus 20 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 02:15:03 -04:00
Juan DenisandClaude Fable 5 4ca07fb71c feat(extensions): per-plugin config store + Configure form (#49)
Completes the deferred half of #30. InstalledPlugin gains config_json
(migration 045); admin-gated GET/PUT /api/v1/plugins/<id>/config
persists values; plugins read them via the new plugins_sdk.config(slug)
accessor. The Marketplace Installed tab shows a Configure button for
plugins declaring a config_schema and renders a real form
(string/number/integer/boolean/enum/secret fields, defaults, hints).
Values may hold secrets so they are excluded from to_dict() and only
served by the admin endpoint. Documented in docs/EXTENSIONS.md.

2 new tests (config round-trip + SDK accessor + secret non-leak;
non-object rejection); pipeline suite 18 green; lint 0 errors; build
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 02:09:39 -04:00
Juan DenisandClaude Fable 5 1c911b50df feat(extensions): installed plugins survive panel updates (#48)
A panel update deploys a fresh clone/tarball preserving only .env and
the DB, wiping app/plugins/<slug> + src/plugins/<slug> for any
URL/registry/upload-installed extension — the surviving InstalledPlugin
row then flipped to error at next boot. Two-layer fix:

- update.sh: preserve_installed_plugins carries user-installed plugin
  dirs from the old tree into the new one (skipping anything the new
  tree already ships and __pycache__), before the frontend build so
  carried UIs recompile. Covered by harness test T18.
- boot repair pass (plugin_service.repair_missing_plugins, runs before
  load_all_plugins): builtin slugs reinstall from builtin-extensions/,
  url installs re-download from source_url (force + no hot-load so the
  loader registers once), sourceless uploads get a clear re-upload
  error. install_from_url/path grew force/hot_load plumbing.

4 new repair tests (16 total in the pipeline suite); update.sh harness
20 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 02:03:33 -04:00
Juan DenisandClaude Fable 5 36b6a92900 feat(extensions): convert Cloud Provisioning, Remote Access and Status Pages to builtins (#45-#47)
Three more tab-group conversions on the #43 primitive, completing the
plan''s Phase 1 seed list:

- serverkit-cloud-provision — "Cloud Servers" tab in the Servers group
  (order 4 keeps its original strip position)
- serverkit-remote-access — "Remote Access" tab in the Servers group
  (order 5), plus the command-palette entry the core page never had;
  pages/RemoteAccess.jsx stays core (the server detail page embeds it
  with serverId, independent of install state)
- serverkit-status — "Status Pages" tab in the Observability group; the
  public /status/:slug route and PublicStatusPage stay core

Backends stay core per D2 (two-speed). All three added to
CONVERTED_BUILTIN_SLUGS so upgraded panels auto-install them once;
fresh installs see them in the Marketplace (10 builtins total).

Full backend suite 1215 passed; lint 0 errors; build clean; builtin
frontend sync --check green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 01:54:21 -04:00
Juan DenisandClaude Fable 5 50166b8f78 feat(extensions): tab-group contributions (#43) + FTP as serverkit-ftp (#44)
Extensions can now contribute a real tab to a core-owned tab group:
a new `tabs` contribution kind ({group, to, label, icon?, end?, order?},
group id == sidebar item id) is merged into the strip by TabGroupLayout
(new groupId prop; core wins `to` collisions), and route contributions
gained a `group` field so ExtensionRoutes collects them per group and
App.jsx mounts them INSIDE that group''s TabGroupLayout — the shared
PageTopbar chrome stays and the Sidebar keeps the group item lit via
contributed matchPrefixes. Wired for files/servers/monitoring.

First consumer: the FTP Server page moves to the serverkit-ftp builtin
extension (tab in the Files group; backend /api/v1/ftp stays core per
D2). Added to CONVERTED_BUILTIN_SLUGS so upgraded panels auto-install
it once; fresh installs find it in the Marketplace.

Proven by the extended contributions-envelope test (26 plugin/platform
tests green; full backend suite 1215 passed), lint 0 errors, build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 01:39:37 -04:00
Juan DenisandClaude Opus 4.8 8dba586b0e feat(extensions): Phase 5 — extract Cloudflare zone-ops into serverkit-cloudflare-ops
Moves the Cloudflare per-zone control panel (zone settings, cache purge, WAF,
Workers, Tunnels, R2/KV/D1 storage) out of core into the bundled, default-installed
`serverkit-cloudflare-ops` extension (#36). Much cleaner than WordPress:

- Backend: only 2 files move — `api/cloudflare.py` + `cloudflare_service.py` →
  `builtin-extensions/serverkit-cloudflare-ops/backend/` (blueprint keeps
  /api/v1/cloudflare, D9; the one intra-extension import made relative). ZERO core
  callers (the service's only importer was its own blueprint), so no bridge
  accessors are needed for core — `cloudflare_ops_bridge.py` exists only so the
  moved test can reach the service by a dash-safe importlib path.
- No duplicate Cloudflare client (the hard constraint): the moved CloudflareService
  still borrows the single core CloudflareClient via DNSZoneService._resolve_credential.
  The whole DNS layer (dns/cloudflare.py, dns_zone_service, dns_provider_service,
  DNSProviderConfig) and the CloudflareWorker/CloudflareTunnel models (FK into
  dns_provider_configs) STAY core.
- Flagship (default-installed, uninstallable): added to FLAGSHIP_SLUGS — zero nav
  footprint, and the core Domains "Open in Cloudflare" button depends on the route.
- Frontend: a single plain route `cloudflare/zones/:zoneId` contributed via the
  manifest + a re-export shim; the page/panels/api-client/SCSS stay in core
  (Pattern A). Core App.jsx import + route removed.
- Retired the core blueprint registration; the plugin status guard is the gate.

Verified: 80 tests green (new tests/test_cloudflare_extraction.py + the moved
test_cloudflare_zone_settings.py + the 4 DNS-layer tests that stay core and still
exercise the shared client); npm run lint + build clean; the backend serves the
`cloudflare/zones/:zoneId` route contribution from the seeded flagship row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 01:04:03 -04:00
Juan Denis 15b2f7faf4 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-07-02 00:54:02 -04:00
Juan DenisandClaude Opus 4.8 39d21f7a46 feat(extensions): Phase 5 — WordPress UI contributed by serverkit-wordpress
Completes #38: the WordPress frontend now belongs to the extension too, not just
the backend. The tab-group constraint (contributed routes are flat — there's no
way to contribute a shared TabGroupLayout parent inside the dashboard) is solved
by contributing a SINGLE splat route `wordpress/*` whose component
(WordPressExtension) self-renders the whole WordPress sub-router — reproducing the
exact structure core App.jsx used to own:
  - TabGroupLayout + WORDPRESS_TABS over the 3 list pages (sites / plugin library /
    pipelines),
  - full-bleed site + pipeline detail routes as siblings,
  - the legacy /wordpress/projects → /wordpress/pipelines deprecation redirects.

The 5 page components stay in core pages/ and are re-used (Pattern A, same as
serverkit-git/email/workflows); only the routing + nav/palette/title ownership
moves into the manifest.

- builtin-extensions/serverkit-wordpress/frontend/index.jsx (source) → synced to
  frontend/src/plugins/serverkit-wordpress/ (pre-bundled, D5) via
  scripts/sync-builtin-frontends.mjs (drift check green).
- plugin.json contributions: nav (WordPress), routes ([{path:"wordpress/*",
  component:"WordPressExtension"}]), page_titles, command_palette.
- Core App.jsx: removed the WordPress imports, the TabGroupLayout WORDPRESS_TABS
  route block, the detail routes, LegacyWpPipelineRedirect, and the now-unused
  ModuleRoute + WORDPRESS_TABS imports (kept the dynamic /wordpress/ title
  fallbacks). Removed the sidebar nav item and the two command-palette entries.
  WordPress now disappears cleanly (nav+routes+API) when the extension is
  uninstalled.

Verified: npm run lint (0 errors) + npm run build clean; the screenshots skill
renders /wordpress/42 correctly (visually confirmed — full detail page, Overview
tab, sidebar item active); backend flagship still seeds/loads (has_frontend now
true) with tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:50:10 -04:00
Juan DenisandClaude Opus 4.8 a55e50bc16 feat(extensions): Phase 5 — extract WordPress into serverkit-wordpress (backend)
The largest single move in the plan (#38). The entire WordPress backend leaves
core for the bundled, default-installed serverkit-wordpress extension (D4).

Moved (12 files → builtin-extensions/serverkit-wordpress/backend/, intra-extension
imports made relative, core imports stay absolute):
  api/wordpress.py, api/wordpress_sites.py, api/environment_pipeline.py,
  wordpress_service.py (2676 lines), git_wordpress_service, wordpress_env_service,
  wordpress_plugin_library_service, wp_analytics/reports/security/update/vulnerability.

Wiring:
- Blueprints keep /api/v1/wordpress, /wordpress/projects and the /wordpress/pipelines
  alias (D9) via new manifest `extra_blueprints` support (3 blueprints / 4 mounts).
- Flagship (D4): seeded active on every panel (fresh + upgrade), uninstallable,
  never deletes tracked builtin/pre-bundled files; the `wordpress` module toggle is
  retired (MODULES now empty) — the plugin status guard is the gate.
- Loadability: the dashed package app.plugins.serverkit-wordpress is registered
  in-place via spec_from_file_location (no copy) — uniform across prod/dev/test.
- The 11 WP test files re-pointed through app.services.wordpress_bridge; the 6 lazy
  core callers were re-pointed in the prior commit.
- _regenerate_frontend_manifest() is a no-op under testing so the flagship boot
  seed can't clobber the checked-in plugins-manifest.json.

Deviations (documented, matching the Email precedent):
- WordPress *models* stay core — status_page/environment_activity/promotion_job hold
  FKs into wordpress_sites, so relocation would be a breaking data migration.
- Event catalog + Fail2ban WP filter stay core as two-speed items (extension calls
  them via core APIs); inverting adds risk without blocking the move.
- Frontend UI stays core this session (it lives in a TabGroupLayout — the tab-grouped
  conversion Phase 1 deliberately deferred); the extension is backend-only, so there
  is no route duplication. Frontend contribution is the scoped follow-up.

Verified: clean collection across all 1223 tests; 199 passed / 2 skipped / 0 failed
across the WP + plugin/extension + module + backup + core-caller parity set, incl.
the new tests/test_wordpress_extraction.py proving the move.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:15:10 -04:00
Juan DenisandClaude Opus 4.8 eee21d8a14 refactor(plugins): add WordPress bridge + flagship/multi-blueprint plumbing
Pre-reqs for extracting WordPress into a bundled default-installed extension
(#38). No behavior change yet — WordPress still ships as core; this only inverts
the couplings so the physical move is mechanical:

- app/services/wordpress_bridge.py: importlib bridge to the (future) dashed
  package app.plugins.serverkit-wordpress, with a two-speed fallback to the
  legacy core location so callers work before AND after the move.
- plugin_service: _ensure_builtin_backend_importable() registers a dashed builtin
  backend in-place via spec_from_file_location (no copy) — solves dev/test/prod
  loadability of a default-installed dashed package; extra_blueprints manifest
  support (WP needs 3 blueprints / 4 mounts incl. the /pipelines alias);
  FLAGSHIP_SLUGS + seed_flagship_extensions() + flagship-aware install/uninstall
  (D4: default-installed, uninstallable, never deletes tracked files).
- create_app: seed flagships before load_all_plugins (inert until the folder exists).
- Re-point the 6 lazy WP-service callers (preview/webhook/env-pipeline/apps/
  builtin_handlers/backup_policy) through the bridge; drop the eager
  WordPressService import from app/services/__init__.py. Models stay core.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:45:22 -04:00
github-actions[bot] 63aea54446 chore: bump version to 1.7.6 [skip ci] 2026-07-02 03:22:58 +00:00
Juan DenisandClaude Opus 4.8 fe152ae6f2 fix(sidebar): Marketplace is always visible, not hidden by onboarding presets
Marketplace was in ADVANCED_ITEM_IDS, so every preset except "Full" (including
the default "Recommended" and everything the Setup wizard's use-case picks map
to) hid it — users had to switch presets/customize to find it. It's the front
door to extensions, so mark it alwaysVisible (like Dashboard) and drop it from
ADVANCED_ITEM_IDS. Now shown regardless of the onboarding choice or a custom
config that predates this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:19:41 -04:00
Juan DenisandClaude Opus 4.8 e3249fbd52 fix(system): report IANA timezone_id; guard chart formatting against bad zones
SystemService.get_server_time fell through to Python's time.tzname on hosts
without /etc/timezone or timedatectl (e.g. Windows dev), yielding a display
name like "Eastern Daylight Time" — not a valid IANA/Intl key. The dashboard
MetricsGraph then crashed with RangeError: Invalid time zone specified.

- Backend: add a cross-platform tzlocal fallback + an IANA-validity guard so
  timezone_id is always an IANA key or null (display name stays in
  timezone_name). Declare tzlocal in requirements.
- Frontend: MetricsGraph validates the zone and falls back to local time
  instead of throwing (defense in depth).
- Test: test_server_time_timezone.py.

ServerKit's timezone feature (get/list/set via /api/v1/system/timezone) is
unchanged — this only fixes the value reported on non-Linux hosts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:55:11 -04:00
Juan DenisandClaude Opus 4.8 b59a57430e feat(extensions): Phase 4 — extract Email into serverkit-email extension
Moves the mail-server stack out of core into builtin-extensions/serverkit-email/:
- api/email.py + email_service/dovecot/dkim/spamassassin/roundcube → extension
  backend (relative intra-extension imports; core imports absolute); blueprint
  keeps /api/v1/email (D9). Core __init__ no longer registers email.
- Stays core (deviations from the audit, documented): postfix_service (the
  outbound relay actively uses it) + all email models (no external importers,
  so no DNSProviderConfig relocation / data migration needed).
- Frontend: serverkit-email manifest contributes nav/route/title/palette; core
  App.jsx/sidebar/palette entries removed; Email page re-exported (D2 pattern).
- #34 auto-install gated on real mail usage (EmailDomain/EmailAccount rows);
  the core Email module toggle is retired in favor of the extension.
- Tests: test_email_extraction.py proves core has no /api/v1/email, install
  registers+responds (exercises relative-import rewiring), uninstall clean.
  Full backend suite: 1200 passed. Frontend lint 0 errors + build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:39:18 -04:00