Both used psutil without importing it, so every call has been a 500
since they were written. Nothing calls them: the process list and
service status come from /processes and /processes/services.
328 more routes used a JSON body as a dict straight from
request.get_json(), so a body of null, [] or a bare string crashed them
into a 500. Bare reads become json_object(); the
'get_json(silent=True) or {}' reads of action routes become
json_object(optional=True), which still treats a missing body as {}.
Routes that take arrays or pass the body through untouched are left
alone. The full backend suite shows no new failures.
Also carries three files' bare section citations (apps, backups,
notifications) and the extended citation guard, which needed them.
/system/health always said healthy, even with the database gone, so
container healthchecks and the installer trusted a panel that could not
serve; it now answers 503 when SELECT 1 fails. It also gave anonymous
callers the canonical domain and whether secrets encryption is set up;
those details now need a valid token (the Settings page sends one).
33 routes wrapped their body in try/except Exception only to return
unexpected_response(e), which records the crash and answers the same
body the global 500 handler already does. Every site was directly in a
route function, so the handler sees the same exceptions. SAML metadata
relied on its wrapper for an unconfigured IdP; that is now a typed 404
instead of a logged crash on a public, probed route.
Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
GET /system/storage measures where the panel host's disk goes (app
images and volumes, build cache, panel database, backups, logs, the
previous install) and returns the retention settings that bound
ServerKit's own share, for the Storage page.
The About tab could only detect a new release and link to it; applying the
update meant SSH and scripts/update.sh. Wire that script to the UI:
- panel_update_service launches update.sh via systemd-run in its own
transient unit — a plain subprocess child would be killed when the
updater restarts the panel's own systemd unit. Status is stateless
(transient-unit state + the updater's own update-*.log) so it survives
the mid-update backend restart.
- Capability gates keep the button honest: root systemd installs only.
Docker installs get the docker compose pull instructions; dev servers
and hosts without systemd-run get a reason instead of a button.
- GET/POST /api/v1/system/update (admin_required, confirm-gated,
audited). API surface doc regenerated.
- About tab gains Update Now with inline confirm, live log tail while
the update runs, a restarting phase that tolerates the panel being
briefly unreachable, and success/rollback/timeout outcomes. New
aboutTab locale keys in en + es.
The heavy lifting (blue/green switch, migrations, health check,
automatic rollback) stays in update.sh, which is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expose disk_reclaim_service behind /api/v1/system/disk/reclaim: a
read-only report and a confirm-gated POST whose keys are re-validated
against a fresh scan server-side (safe candidates only — telemetry
stays CLI-only). ?wait=false runs it as a background job; both paths
are audit-logged.
The curated fix shows up where the problem is reported: a failing
disk.headroom doctor check and an alerting disk tile each offer
'Reclaim space', which previews measured candidates before handing
the run to Operations.
Twenty-one imports whose last caller left during the plan-76/77 sweeps:
mostly 'import subprocess' in services now routed through the shared
runners, plus time/requests in the CachedRemoteIndex adopters, threading
in agent_gateway, os in api/system, User in api/bandwidth, run_privileged
in nginx_advanced_service, and the dead SENSITIVE_SETTING_PARTS alias in
audit_service (zero importers). Each removal AST-verified unused before
deletion; all modules compile and the app factory boots.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
config.py's parsers move to app/utils/env.py and are imported back; the
three incompatible truthiness variants collapse to one TRUTHY tuple.
Fixes the measured bug: SERVERKIT_ALLOW_PRIVATE_DOWNLOADS=on silently
read false in plugin_service (its tuple lacked 'on'); same for the pip
gate, SERVERKIT_STAGING, and SERVERKIT_DEMO_MODE. Ratchet: no new
os.environ truthiness tuples outside utils/env.py.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan 76 milestone A, the third row. 23 handlers decided admin access inline,
all in two files, in two shapes.
18 were the mid-route gate this milestone already built a door for:
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
which is four lines to say require_admin_user(), and carried the two-line
identity lookup with it. Migrating them closes both rows at once: the identity
census drops 87 -> 69 and the controller-boundary baseline loses 18 persistence
crossings (528 -> 510), with no new entries.
It also fixes a latent 500: where the check omitted the `not user or` guard, a
token naming a deleted user hit `None.role` instead of answering 403.
The other 5 are `not user.is_admin and app.user_id != user.id` — admin OR
owner, which is a different question. Those keep their shape and only converge
the truth source from `role != 'admin'` to `is_admin`; routing them through
can_access_app would silently widen access, since that also honours resource
grants.
The migration initially left `current_user_id` referenced but unbound in four
places. Two were caught by tests; the other two (create_manual_app,
upload_app_archive) have no coverage and were caught by an AST scan for names
used-but-never-assigned. Worth remembering that a mechanical removal needs the
scan, not just the suite.
Gate: -k "system or apps or app_ or host_snapshot or manual_app or upload",
304 passed / 13 skipped; all seven ratchets green (52 passed).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A VPS resized from 1 vCPU/1 GB to 4 vCPU/8 GB with a 60 GB volume attached,
and the panel never mentioned any of it. Not a stale cache: a resize forces a
power-off, so the process and its in-memory spec cache die and the next boot
reads the truth. The gap is that nothing ever persisted the PREVIOUS numbers,
so there was nothing to compare against — and the reboot that causes the
change is what destroys the only copy of the baseline.
Adds host_snapshots: capture specs + per-filesystem inventory at boot, diff
against the last row, and emit host.specs_changed / host.storage_advisory on a
real delta. Advisories fire on state transitions only, so an unfixed fstab
entry does not nag on every restart.
Repairs a divergence this exposed: resource_tier_service measured the first of
/var/lib/serverkit,/var/lib/docker,/ while capacity_service hardcoded /. Both
now use host_inventory_service.data_path(), and local headroom reports which
mountpoint it measured.
Notifications render in the existing bell UI, so no frontend change is needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- install_profile_service: record what install.sh put on the box
(minimal/standard/full), recommended from detected hardware and
overridable; thresholds mirrored by recommend_profile in install.sh
- setup wizard: replace the hard ResourceGate/SetupStepTier with
SetupStepCapacity + SetupStepSecurity
- ResourceAdvisory: inline capacity warning at the point of action
(WordPress/Docker/Services/Deployments), backed by headroom.fits
- RequiresDocker component for docker-dependent surfaces
- install.sh/update.sh: profile detection + tests
(test_install.sh, test_update.sh, test_server_capacity.py)
/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>
- Add GET /api/v1/system/notices endpoint (admin-only)
- Detect missing canonical domain, sites base domain, server public IP, and wildcard HTTPS
- Add SystemNotices React component with dismiss persistence in localStorage
- Render notices inside DashboardLayout so they appear on every page
- Add scoped SCSS for warning/info/error notice banners
- Auto-generate SERVERKIT_ENCRYPTION_KEY in production when missing.
- Detect canonical domain from Host/X-Forwarded-Host; persist via SystemSettings.
- Expand CORS origins dynamically to include the canonical domain.
- Add SystemTab UI to view/save detected panel domain and HTTPS preference.
- Install script writes key, prompts for PANEL_DOMAIN, and sets nginx IP→domain redirect.
- Add backend tests for key bootstrap and domain detection helpers.
Replace generic @jwt_required() usage with role-based decorators and switch Socket.IO token handling to the auth payload.
- Replace jwt_required with viewer_required/admin_required across API modules (backend/app/api/cron.py, firewall.py, ftp.py, git.py, metrics.py) and update imports accordingly.
- Require JWT for previously public system/private endpoints (backend/app/api/system.py, private_urls.py).
- Change Socket.IO server connection handler to accept auth payload and read token from auth (backend/app/sockets.py) to avoid token leakage in query strings.
- Update frontend clients to send token via socket auth instead of query (frontend/src/pages/WordPressProject.jsx, frontend/src/services/socket.js).
These changes add finer-grained RBAC control for endpoints and improve socket auth security by removing tokens from URLs/logs.
Introduces backend logic to determine server resource tier and block WordPress site creation on insufficient resources (lite tier). Adds ResourceTierContext and ResourceGate component to the frontend, displaying a UI warning and disabling creation actions when requirements are not met. Updates WordPress page and styles to integrate the new resource gating mechanism.
Added system info API for admin, improved system metrics with cached memory and network I/O stats, and updated frontend dashboard with refresh controls and live/polling status. Workflow builder now supports loading full server infrastructure, adding real apps, and improved node palette. Docker app nodes and config panels display more details for real apps, and styles updated for new UI elements.
Introduces backend API endpoints and service methods for retrieving and setting server time and timezone (admin only). Updates the dashboard to display current server time and timezone, and adds a timezone selector to the settings page for admins. Includes supporting API methods and frontend styles for the new UI components.
Backend:
- Add /system/check-update endpoint
- Fetch latest release from GitHub API
- Compare versions and return update_available flag
- Cache results for 1 hour to respect rate limits
Frontend:
- Add checkUpdate() API method
- Add "Check for Updates" button in About section
- Show update available notification with release link
- Show "You're up to date" when current
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduces a new /system/version API endpoint in the backend to provide the current ServerKit version, reading from possible VERSION files. Updates the frontend About section to fetch and display the version dynamically instead of hardcoding it.
Add backend and frontend foundations for ServerKit, including Flask API structure, SQLAlchemy models, service layers, and React app with authentication context and service hooks. Implement core API endpoints for authentication, application management, Docker, databases, and system operations. Update README with project overview, tech stack, and setup instructions. Add development roadmap and example environment configuration.