1039 Commits
Author SHA1 Message Date
Juan Denis b86c03dc6b Don't 500 on uptime stop when its data dir is unwritable
Stop now persists the tracking flag, and the save created the data
directory unguarded. Where that fails (an unprivileged host or CI
runner) the save is now logged and reported false, as its docstring
already promised, instead of raising.
2026-10-11 00:52:44 -04:00
Juan Denis 498901e70e Keep the panel booting when uptime can't resume
Resuming uptime tracking at boot read the history through a helper
that first created /var/lib/serverkit. The image runs unprivileged, so
that makedirs raised and the app never started. Reading now creates
nothing, and a failed resume is logged instead of aborting boot.
2026-10-11 00:50:14 -04:00
Juan Denis 6cfebf8a54 Let the load-average test run where os lacks it
Windows has no os.getloadavg, so patching it raised before the test
could run; create=True supplies the attribute.
2026-10-10 23:44:20 -04:00
Juan Denis 3e63a7f4b7 Use the platform's EAI_NONAME in the DNS test
The test raised errno -2, Linux's value, so on Windows (11001) the
check read it as a resolver failure and the test failed.
2026-10-10 23:44:00 -04:00
Juan Denis f3145289c8 Time out version and status probes after 60s
--version, which, postconf, pip list/freeze and mailq had opted out of
the run_checked default with timeout=None, so a wedged binary held a
request thread forever. Installs, migrations, dumps and restores keep
no timeout.
2026-10-10 22:22:40 -04:00
Juan Denis 6400b76a7d Drop imports that shadowed module-level names
Function-local re-imports of User, WorkspaceService and secrets, and a
local list named secrets, hid the module names they duplicated.
2026-10-10 22:20:03 -04:00
Juan Denis 291b5df844 Document every panel environment variable
About 30 of the ~70 variables the backend reads were documented
nowhere: the encryption-key file, extension signing keys, the pip
opt-in, the setup-code pin. docs/ENVIRONMENT.md lists them all with
defaults, and a guard fails when the backend reads one it doesn't list.
2026-10-10 22:13:09 -04:00
Juan Denis f840744341 Drop an unused install-dir constant
disk_reclaim_service read SERVERKIT_DIR, which app/paths.py treats as
the /var/serverkit data root, and never used the value.
2026-10-10 22:10:27 -04:00
Juan Denis dc81185b7d Lower the JWT-only ceiling to 598
Two JWT-only routes were removed with the dead /system endpoints.
2026-10-10 22:05:38 -04:00
Juan Denis bee7a4a1aa Fix stopping an app that deploys on compose slots
A function-local import of SlotDeployService further down
stop_application made the name local to the whole function, so the
compose-slot branch raised UnboundLocalError before reaching it: Stop
on such an app always failed. The module already imports the class.
2026-10-10 21:55:33 -04:00
Juan Denis 9d750a4f6f Remove the /system process and service routes
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.
2026-10-10 21:51:15 -04:00
Juan Denis 99a19c2fb7 Read 40 more route bodies through json_object()
The first pass left routes that hand the body straight to a service
(create_zone(data), MonitorService.create(data), ...). Those services
read it as a dict, so a body of null, [] or a bare string still
crashed them into a 500. Agent long-poll, external webhook receivers
and the routes that already type-check their body are left alone.
2026-10-10 21:48:18 -04:00
Juan Denis 61994b248e Trim history from the deploy preflight docstring 2026-10-10 21:34:02 -04:00
Juan Denis 95e2e6e6da Move extension zip fetching into plugin_fetch
GitHub release resolution and the SSRF-guarded download (redirect
re-validation, byte caps, token scoping) were 250 lines of
plugin_service unrelated to installing. plugin_service re-exports the
names its own code calls, so existing monkeypatches on it still land;
tests that exercised the fetch internals directly now target
plugin_fetch.
2026-10-10 21:32:19 -04:00
Juan Denis 3451d5ba2c Split TemplateService into mixin modules
template_service.py held port allocation, magic-variable resolution and
the template catalog alongside install/update in one 2,800-line class.
Those three groups move verbatim into TemplatePortsMixin,
TemplateMagicMixin and TemplateCatalogMixin, each in its own module;
TemplateService inherits them, so its surface (106 attributes, same
classmethod/staticmethod kinds) and every caller are unchanged. Tests
that patched module globals now patch the module the code lives in.
2026-10-10 21:10:14 -04:00
Juan Denis b35f6df97a Validate the journal priority parameter
The value went straight to a privileged journalctl -p. As its own argv
element it could not inject options, but anything other than levels
0-7 now gets a 400 instead of a journalctl error.
2026-10-10 20:54:45 -04:00
Juan Denis ec8cd9462e Bound Docker metadata reads with a timeout
ps, inspect, info, version, tag and the network/volume inspects ran
with timeout=None, so a wedged dockerd held every request thread that
asked, forever; the panel runs a single worker process. They now give
up after 30s, and the Containers page reports Docker unreachable.
Pull, build, logs and compose stay unbounded on purpose.
2026-10-10 20:54:12 -04:00
Juan Denis 55d8fb1e44 Read route bodies through json_object()
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.
2026-10-10 20:53:43 -04:00
Juan Denis ddaeb549ad Remove bare plan-section citations
Comments cited plan sections as "(§G3)" or "(§8 unification)", which
point into the same git-ignored plans the plan-number citations did.
References next to a real document (docs/REDESIGN_MAP.md §3) stay. Code
and runtime strings are unchanged (AST and token comparison).
2026-10-10 20:25:49 -04:00
Juan Denis e94537d0a4 Cut docstrings that narrate refactor history
The worst cases the audit named: docstrings and header comments that
told how three old copies drifted, or quoted subprocess counts that
were stale the day they were written, instead of stating the contract.
Each now says what the function does and keeps one line of why. Also
drops the always-zero n_cloud counter that existed only to keep a boot
log line's or-chain intact.
2026-10-10 20:21:00 -04:00
Juan Denis 166e9acabf Split servers.py into area modules
The 3,000-line file mixed core server routes with Docker, agent
install/update/fleet and remote host operations. Those sections move
verbatim into servers_docker, servers_agents and servers_remote on the
same blueprint; the registered route map (127 rules, methods and
endpoint names) is identical before and after.

The controller-boundary scan only treated files that create a
Blueprint as controllers, so an area module would have escaped it; it
now also counts files that register routes.
2026-10-10 18:49:59 -04:00
Juan Denis bf5405df9e Move the health check's database probe into a service
The previous commit ran SELECT 1 directly in the route, a new
persistence crossing in the API layer that the controller-boundary
ratchet rejects.
2026-10-10 18:48:53 -04:00
Juan Denis 13c34f3f8f Drop the unused apispec-webframeworks dependency
Nothing imports it; the OpenAPI service builds its spec with apispec
and its marshmallow plugin only.
2026-10-10 18:34:42 -04:00
Juan Denis 587dcec12a Make the health check probe the database
/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).
2026-10-10 18:23:24 -04:00
Juan Denis d134af25ae Drop route try/excepts the 500 handler duplicates
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.
2026-10-10 18:14:28 -04:00
Juan Denis 98bc6946f9 Bound the generate-password length parameter
Any signed-in user could pass ?length=1000000000 and make the server
build a gigabyte-long password string. Lengths are clamped to 8-128.
2026-10-10 18:02:03 -04:00
Juan Denis e40e24b4f1 Map remote agent failures to one status rule
The cron proxy answered an offline agent with 503 and a refused command
with 403, while 22 of the 24 Docker proxy routes answered 500 for both,
so the UI could not tell an offline agent from a crash. Every remote
handler now uses the same rule.
2026-10-10 18:01:02 -04:00
Juan Denis 3e1a264ee4 Reject non-object JSON bodies with a 400
Eleven routes passed request.get_json() straight to data.get(...), so
a body of null, [] or a bare string crashed them into a 500. They now
read the body through json_object(), which answers a typed 400.
2026-10-10 17:04:21 -04:00
Juan Denis d8f1d5b2d5 Let the global handler render queue-bus errors
QueueBusError is an expected, caller-actionable failure, so it now
subclasses ApplicationError and the global handler answers it with its
own status. The 21 queue-bus routes each wrapped their whole body in
try/except only to do that by hand. Also records the two workspace
queries the previous commit moved out of the API layer.
2026-10-10 16:57:29 -04:00
Juan Denis cc8c74e1ef Compute visible workspace ids in one place
The same "active workspace, else my workspaces, else all for an admin"
rule was written out in projects, environments and search. It now lives
in WorkspaceService, with a request-aware wrapper for routes.
2026-10-10 16:56:42 -04:00
Juan Denis daf6314791 Share one OAuth implementation across git providers
GitHub, GitLab and Bitbucket each carried their own copy of config
read/update, the authorize hop, the code exchange and the stored
connection. One implementation now reads the few real differences from
a per-provider table (Basic-auth token exchange and URL-less scopes for
Bitbucket, no grant_type for GitHub, profile field names). New tests
pin those differences; they passed on the old code before the change.
2026-10-10 16:43:14 -04:00
Juan Denis 6068b099dc Replace nine per-app access helpers with one
Nine route files each loaded the live app and checked access, with
four return shapes, docstrings promising error tuples the code never
returned, and 41 dead 'if err: return err' branches. require_app()
backed by ResourceGrantService.get_app_for does it once; conceal=True
keeps the 404-for-denied answer the previews, WAF and slot routes
already gave, so no route's status changes. Nine query sites leave the
API layer.
2026-10-10 16:36:01 -04:00
Juan Denis d7234b822d Delete nine backend functions with no callers
Each was referenced only by its own definition across the panel, its
tests, scripts, builtin extensions and every sibling extension repo
(~350 lines). Imports they alone used go with them.
2026-10-10 16:09:29 -04:00
Juan Denis 6296887bad Remove internal plan citations from app code
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.
2026-10-10 15:50:45 -04:00
Juan Denis 4faac6e1cd Check the tail stand-in by pid, not a Popen wrapper
The Popen wrapper counted as a hand-rolled subprocess stub and pushed
the adoption ratchet over its ceiling. The stand-in now records its own
pid and the test asks psutil whether it is gone.
2026-10-10 14:32:03 -04:00
Juan Denis c1f909b966 Log best-effort failures instead of dropping them
A lost "backup failed" alert, a broken live deploy log and missing
deploy or build log lines were all except-pass, so none left a trace.
They still never raise; live-emit failures report once per kind so a
dead socket path is findable without flooding the log.
2026-10-10 14:26:59 -04:00
Juan Denis b9456d1a9a Stop status pages claiming 100% with no data
Uptime columns defaulted to 100.0 and the pages formatted a missing
value as 100.00%, so a new monitor showed a perfect month. The average
also used 'uptime || 100', turning a fully down component (0%) into
100%. Missing data now reads as a dash and 0 stays 0; both status pages
share one helper.
2026-10-10 14:17:41 -04:00
Juan Denis c44da15993 Hold allocated ports until the install claims them
The allocator checked app rows, containers and a bind test, none of
which exist while an install is still being planned, so concurrent
installs could be handed the same port. Port variables now claim the
port for 15 minutes and every lookup skips live claims. An exhausted
range raises a 409 instead of returning a random port that could
collide with anything.
2026-10-10 14:14:09 -04:00
Juan Denis 9b0323ebfb End tail -f when a log stream stops
The stop flag was checked between lines, but readline() blocks until
the log grows, so closing the viewer on a quiet log left a sudo tail -f
running for the life of the panel. A watcher now kills tail on stop.
The socket emitter also gave up after 30s without a line, silently
leaving the client subscribed to nothing; it now runs until the stream
is stopped or replaced.
2026-10-10 14:07:22 -04:00
Juan Denis e2ba3a3476 Keep corrupt configs and serialize config writes
A corrupt JSON config read as the defaults and the next save, which
writes back what was loaded, destroyed the only copy of the operator's
settings; it is now moved aside first. Every save also used the same
fixed temp name, so two concurrent writers could rename a half-written
file into place; each write now gets its own temp file and writers to
one path are serialized.
2026-10-10 14:04:09 -04:00
Juan Denis db5f4df74a Show Docker list errors and ignore stale loads
The API now answers 503 when docker ps fails instead of an empty list,
so the Containers page no longer says "No containers" on a host
running many. The Containers and Images tabs show that error, and a
late answer from the previously selected server can no longer replace
the current server's list. Also fixes the local branch of the remote
Docker listing, which passed all= to a parameter named all_containers
and always failed.
2026-10-10 14:01:56 -04:00
Juan Denis 5fb35e8cc0 Fail scheduled backups whose offsite upload fails
The upload helpers report failure in their return value, which the
scheduler ignored: the schedule read success and no failure alert went
out while the only copy sat on the box being backed up. Auto-upload
failures are now logged instead of silently dropped.
2026-10-10 13:46:17 -04:00
Juan Denis 44e4351cc4 Remove the unused wp-cli.phar
Nothing references it, yet at 7 MB it was the largest tracked file and
shipped in every image through COPY backend/.
2026-10-10 13:41:03 -04:00
Juan Denis b739a6b17e Drop gevent and the stale backend Dockerfile
The panel runs async_mode='threading' under a plain threaded gunicorn
worker; nothing imports gevent. It was still installed everywhere, and
on Python 3.14 (no wheel yet) its source build is what broke installs
in issue #148. backend/Dockerfile was unreferenced and still launched
the gevent-websocket worker that double-answers the WS handshake; the
run.py hint recommended the same worker.
2026-10-10 13:40:55 -04:00
Juan Denis 47df5167cb Recover jobs stranded by a panel restart
The broker only delivers pending messages, so anything in flight when
the process died was never delivered again and its Job row read
running forever. At boot, before any consumer polls, interrupted jobs
are failed visibly (a half-run deploy or restore is not safe to repeat
blind) and every other stranded message is redelivered or, when out of
attempts, dead-lettered.
2026-10-10 13:21:22 -04:00
Juan Denis 0bcdb229d7 Drop the retired workflow-schedules job
The handler became a no-op when the workflow engine was retired, but
its schedule kept firing every 60 seconds, writing about 1,440 empty
job rows a day. Migration 110 removes the schedule row and cancels any
job still queued for it.
2026-10-10 13:12:09 -04:00
Juan Denis e6658d70b3 Resume monitoring and uptime tracking at boot
Both loops only started from an API call, so every restart silently
stopped threshold alerting while the UI still showed it enabled. Uptime
tracking now persists its on/off flag, the monitor loop runs inside an
app context so its telemetry writes stop failing, and SMTP gets a
timeout so a dead mail host cannot wedge the loop.
2026-10-10 13:08:32 -04:00
Juan Denis 6901a4ff91 Run fleet threads inside the live app
Rollout and queued-delivery threads called create_app(), rerunning
migrations, plugin loading and the boot reconcile against the live
panel. The upgrade fan-out thread had no app context at all, so its
first query raised and online agents were never told to upgrade.
2026-10-10 13:06:09 -04:00
Juan Denis 051ea4d31d Keep CLI commands from starting the daemons
cli.py runs as a plain python script, so the factory's flask-argv sniff
never matched it: every serverkit verb booted a second job consumer,
scheduler and relay beside the live panel, whose boot reconcile marks
the panel's in-flight deploys failed.
2026-10-10 13:03:37 -04:00
Juan Denis 4e45ecb5f5 Move the new auth lookups behind the model/service
The server access helper and the app-name clash checks queried the DB
from the API layer, which the controller-boundary scan rejects. They
now go through WorkspaceService.get_accessible_server and
Application.name_clash_error; the baseline drops by three.
2026-10-10 09:56:44 -04:00