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>
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>
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>
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>
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>
/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>
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>
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>
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>
- 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>
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>
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>
- 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>
- 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>
- 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>
- 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>
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.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>