Commit Graph
462 Commits
Author SHA1 Message Date
Juan Denis 2f52c87a39 Add /ports/check and /ports/suggest
The deploy paths already know which host ports are taken (app rows,
slot rows, Docker mappings, a bind test) but only used that to pick a
port silently. Exposing it lets the shared port input say "8001 is
used by shop-api" and offer the next free port, instead of failing
at deploy time.
2026-10-03 22:46:54 -04:00
Juan Denis 93a3cd783e Let GET /api/v1/servers accept a scoped API key
Vela holds one ServerKit API key as a host-side secret and polls the
fleet list, so this read route moves from bare @jwt_required() to
auth_required() + require_scope('servers:read') — a deliberate,
per-route jwt_only_census exception, not a sweep. JWT callers are
unchanged; a restricted key without servers:read gets 403 from the
scope middleware, no credentials get 401.

Scope choice: the existing servers:read catalog entry already covers
'list servers, agents, and metrics', and the serverkit-gui read routes
(capabilities/frame/surface, sibling repo) take the same scope, so one
key scope serves the whole Vela polling surface.

The census ceiling ratchets down to 600 and the migration inventory is
regenerated, as both ratchets require. tests/test_servers_api_key_scope.py
pins the four cases: scoped key 200, wrong-scope key 403, JWT 200,
anonymous 401.
2026-10-03 00:31:36 -04:00
Juan Denis c7b6f145d1 Serve the panel through the Cloud relay tunnel
Until now the connect client refused every http and sio stream, so
Open panel in ServerKit Cloud showed a blank page. The client now
bridges them to the loopback listener, the panel serves its page
under the relay's /t/<device>/ prefix, and the SPA prefixes its
router, API and Socket.IO paths from it. The Cloud grant is checked
against Cloud's JWKS before signing in the panel user with the same
email; panel two-factor still applies.
2026-09-29 23:19:34 -04:00
Juan Denis 9eafe6709e Point compose app actions at the live slot project
Plan 87 phase E, compose part. For a compose app on slots, the live
release is a slot project, not the project its own compose file names.
Status maps to the live project only (a stopped standby never reads as
a degraded app), start/stop/restart and sleep/wake act on it, container
listings show the live and standby projects, and an image update runs
as a queued slot deploy instead of recreating the stack in place.
2026-09-28 22:27:13 -04:00
Juan Denis d0b11714e7 Deploy compose apps through A/B slots
Plan 87 phase C. A compose app on slots runs each slot as its own
project, <name>-a / <name>-b, rendered from the merged compose config:
container_name stripped, only the web service published, on the
slot's loopback port, and every named volume pinned to the name it has
today. Without the pin compose prefixes volumes with the project name
and slot B would start on empty ones; the real-Docker leg checks this.

Stateful services (a database, cache or queue with a named volume)
cannot run twice. They move once, on an explicit previewed split, into
a shared <name>-data project that both slots reach by the same names.

The webhook, template-update and rollback paths deploy through the
engine instead of stopping the stack. The --scale web=2 rolling
restart and the Webhook.zero_downtime flag are deleted: it assumed a
service named web, ignored its own health result, and no UI set it.
Compose slot images get immutable per-deployment tags and are pruned
with the rest.
2026-09-28 22:26:44 -04:00
Juan Denis 0749508599 Run release commands and DB snapshots before the switch
Plan 87 phase D. Both slots share the database, so what changes it
runs once, before the switch, while the old release still serves:
the app's managed databases are dumped (a warning, never a blocker),
then the release command runs in a one-off container of the new
image. A failed release aborts the deploy with the live slot
untouched. The command comes from the app's setting, the Procfile
release: line, or serverkit.yaml; a rollback never re-runs it.

Rolling code back never rolls the database back: after a switch
back the slot card offers the pre-deploy snapshot as an explicit
second step with a data-loss warning. stop_old_before_release is
the announced-downtime fallback for schema changes that cannot stay
backward-compatible for one deploy.
2026-09-28 21:41:52 -04:00
Juan Denis f77aaa1f4d Deploy single-container apps through A/B slots
Plan 87 phase B. An app that opts in keeps two slots, like the
panel updater's two installs. A deploy boots the new release in the
idle slot on its own loopback port while the live one serves, holds
it to the health gate, then switches nginx by moving app.port to the
new slot and rewriting the vhost, so every app.port reader follows
without edits. A watch window switches back on its own if the
release falls over; the old slot stays warm for an instant switch
back, then is stopped (not removed) by a standby sweep.

Opting in adopts the running container as slot a with no restart.
Eligibility is shown with reasons and re-checked on every deploy;
opting out folds the slots back on the next in-place deploy. Slot
images are never pruned and slot ports stay reserved.
2026-09-28 21:29:33 -04:00
Juan Denis 87be6be8fa Raise a typed error for bad deploy settings 2026-09-28 21:14:48 -04:00
Juan Denis ddd6772279 Validate deploy settings before touching the app 2026-09-28 21:13:59 -04:00
Juan Denis cecbb743fd Make redeploys fail safe before slot deploys
Plan 87 phase A. Fixes the deploy bugs that make a rollback or a
health check lie:

- builds get an immutable tag per deployment, so rolling back runs
  the image that actually ran instead of whatever :latest became;
  old tags are pruned so this is not a new disk leak
- the deployment records the commit after the pull, not before
- the health wait is a real gate: a 4xx fails, it takes three passes
  in a row, an exited container fails at once, and it raises
- a per-app deploy lock makes a second deploy wait for the first
- a compose rollback rebuilds and recreates its containers, and a
  rollback with no version finds the previous release again
- a failed template update says when restoring the old stack failed

Health path, timeout and the 4xx rule are editable in Settings.
2026-09-28 21:10:29 -04:00
Juan Denis acdf6b878d Remove auto-scaling and the load-balancer builder
Replica counts could be changed, but nginx only proxied to one port
and the auto-scale sweep was never scheduled, so neither the
Auto-scale section nor its five routes could work; the nginx
upstream builder had no UI at all. With one live copy per app they
are gone, and the scale-policy table is dropped. Running containers
are not touched. The scaling spec now says replicas are not
pursued.
2026-09-28 09:03:34 -04:00
Juan Denis e688b7b5c3 Let proxied sites long-cache fingerprinted assets
The Docker vhost set no caching at all. An opt-in rule serves files
whose name carries a content hash with a one-year immutable
Cache-Control, proxying exactly like location /. A name only counts
as hashed with 8+ characters and a digit, so app-settings.js is never
pinned. Off by default: the panel cannot know an app's asset layout.
2026-09-27 22:45:53 -04:00
Juan Denis 3feba262cb Name an app's likely bottleneck on its Overview
A rule-based check reads the app's request metrics, its container
CPU and the CPU and top queries of the databases it references, and
names what fits: page cache for a busy app over an idle database,
a cache or index when one query dominates a busy database, slot
deploys for errors during deploys, and TTL for a low hit ratio. Each
hint links to the fix and states its failure mode; nothing is added
automatically.
2026-09-27 22:36:14 -04:00
Juan Denis 45f7100d93 Offer an opt-in PgBouncer beside PostgreSQL
An installed PostgreSQL can switch on a transaction-mode PgBouncer
sidecar through its managed override, with no host port, on the
shared network. fromService gains pooledUrl; apps keep the direct
connectionString unless they switch, because transaction pooling
breaks prepared statements in some drivers, LISTEN and advisory
locks, and the card says so.
2026-09-27 22:27:32 -04:00
Juan Denis 722072a14b Wire Grafana to Prometheus and Loki by attaching
The Prometheus, Grafana and Loki templates installed but connected
to nothing. A Grafana install now takes Metrics and Logs
attachments: the panel creates the data source through Grafana's
API with the recorded admin login, and a metrics attach also imports
a dashboard of the panel's fleet metrics. An attached app joins the
shared network, so Grafana reaches the source by name.
2026-09-27 21:53:52 -04:00
Juan Denis 776bf7793a Let an installed Prometheus scrape the panel
The metrics endpoint only accepted a PROMETHEUS_TOKEN env var that
almost no install sets, and the Prometheus template's config never
reached its container. The panel now keeps its own scrape token in
an internal vault, hands it to templates as SERVERKIT_METRICS_TOKEN,
and the template ships a prometheus.yml with a serverkit job.
2026-09-27 21:42:18 -04:00
Juan Denis dc08f6975c Show top queries and hit ratios per database
The explorer showed live sessions but nothing about which queries
cost the most or whether the cache was working. A container's new
Insights tab lists top queries by total time (pg_stat_statements,
performance_schema digests) with connections in use and the buffer
cache hit ratio, and says why a list is empty. PostgreSQL's
pg_stat_statements is switched on through the config tuner, keeping
existing preloads, so its rollback undoes it.
2026-09-27 21:30:07 -04:00
Juan Denis 6986f497f3 Attach a cache, storage or queue from the app page
Installed Redis and a running app were unrelated until someone wrote
the env by hand. The Overview's Attached services card reuses an
installed service or installs one, writes the fromService references
(REDIS_URL, AMQP_URL/BROKER_URL, the S3_* set), shows that kind's
failure mode, and offers a redeploy. Detach removes exactly what the
attach wrote.
2026-09-27 21:16:58 -04:00
Juan Denis f23c7a5009 Give each app its own bucket and a scoped key
MinIO's images no longer pull, so object storage is now the Garage
template. Attach storage creates a bucket named for the app and a key
that can read and write only that bucket, keeps the secret in an
internal vault, and wires S3_* and AWS_* env through fromService and
fromSecret. Detach revokes the key and removes the env but keeps the
bucket and its objects. A docker leg proves the scope against a
real Garage.
2026-09-27 20:36:38 -04:00
Juan Denis d148ccc247 Make the micro-cache a real page cache
Each site now sets its own TTL (10s default, 300s cap), and serves
the last good page while the app errors or the entry refreshes, with
one upstream request per missing entry. The cache key carries the
host, so purge clears only this site's files instead of every site's
cache. The settings panel shows the 24h hit ratio.
2026-09-27 18:51:05 -04:00
Juan Denis 11af8a588d Gate request metrics the way the sweeps expect
The route's access check moved into the service, where the per-app
authz sweeps can't see it; it is back in the route. The rollup table
now has a parent-side cascade so a purged app takes its rows, and
reads its histogram through the JSON column mixin.
2026-09-27 18:51:04 -04:00
Juan Denis d2fd3b38b0 Show per-app request rate, errors and latency
A one-minute builtin tick tails each app's timed access log from a
stored byte offset and rolls it into minute and hour rows: status
classes, cache HIT/MISS/BYPASS/STALE and a latency histogram, so
p50/p95/p99 merge exactly across ticks and into hours. Minute rows
go after 48 h and hour rows after 30 days on the same tick. The
Overview tab gets a Traffic card.
2026-09-27 18:18:30 -04:00
Juan Denis d669b7cac5 Add a storage overview endpoint
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.
2026-09-27 16:33:53 -04:00
Juan Denis 45bc3c70bd Guard first admin with a setup code, fix Git deploys
A fresh panel no longer lets whoever reaches it first become admin:
registering the first account needs a one-time code the installer
prints (`serverkit setup-code` repeats it). SERVERKIT_ADMIN_EMAIL and
SERVERKIT_ADMIN_PASSWORD create the admin unattended instead.

Git apps now deploy what users expect: a repository with a compose
file deploys every service it declares, "Deploy latest" builds the
branch's newest commit, and push webhooks rebuild the containers.
Also fixes branch switching on single-branch clones, uploads of a
lone file, compose v1 port preflight, and CLI commands that ignored
their flags.
2026-09-24 00:50:02 -04:00
Juan Denis d866de5439 feat(connect): apply the managed presentation profile (plan 25)
Panel side of the managed profile: the panel receives a presentation
profile from Cloud over the existing command channel, stores it,
acknowledges it, and hides or simplifies fleet-owner-only surfaces
until the profile is lifted or the link is revoked.

- ui.managed_profile command handler with managed-profile.json
  storage, audit entries, and ack-by-output; profile is cleared on
  disconnect and on consent revoke
- GET /api/v1/connect/managed-profile for the local UI
- ManagedProfileContext with capability-driven filtering of sidebar
  items, command palette actions, and sidebar settings; ManagedCard
  explains the managed state
- In-page collapses for Templates deploy, About self-update, and
  Backup schedules/storage (restore stays available)
- Settings gains a ServerKit Cloud tab showing link and profile state

Local verification: 153 backend tests pass, 198 frontend node tests
pass, lint clean, build ok. No live policy has been pushed to a real
panel yet.
2026-09-13 03:50:02 -04:00
Juan Denis fc5a041764 Fix first-run sidebar preferences and extension loading [skip version] 2026-09-11 00:17:39 -04:00
Juan Denis bebc69f71c feat(ai): connect task routing, guarded fallback, and scoped run accounting 2026-09-08 04:09:27 -04:00
Juan Denis 54028fbff4 fix(ai): enforce assistant enable state and conversation cost limits 2026-09-08 03:03:00 -04:00
Juan Denis fde0acee6d fix(ai): satisfy release gates and prepare 1.10.0 [skip version]
Keep connection persistence behind the service boundary, use typed errors and shared JSON parsing, and refresh reviewed inventories. The new GET connection catalog accepts API keys so API clients managing connections can discover IDs without exposing credentials; existing JWT-only routes stay unchanged. Add regression coverage and release notes.
2026-09-08 01:30:03 -04:00
Juan Denis 569b052659 feat(ai): expose Prompture providers and saved connections 2026-09-08 00:42:54 -04:00
Juan Denis d5577e8bda fix(logs): keep remote runtime lookups off the panel host 2026-09-07 22:46:16 -04:00
Juan Denisandjrbertt 4f361fa3d1 Merge PR #136: fix(observability): resolve container runtime logs and metrics
Co-authored-by: jrbertt <joaorebertt0219@gmail.com>
2026-09-07 20:08:16 -04:00
Juan Denis d67e5d07d0 fix: complete audit follow-ups and add regression coverage
Centralize backup schedule calculations and shared request/form flows, encode query values, and move repository finalization into its service. Add browser CI coverage, repair stale test fixtures, and provide opt-in request and build measurement tools.
2026-09-05 14:41:15 -04:00
jrbertt 81a56012c3 fix(observability): resolve container runtime logs and metrics 2026-09-05 08:44:39 -03:00
Juan Denis f490d75bee refactor: share backend lifecycle and fleet metrics operations 2026-09-05 05:06:41 -04:00
Juan Denis 6bf3490345 fix(ai): enforce resource access and bounded protected chat
Scope built-in tools to the live caller, recheck write authorization after confirmation, redact structured results, and fail closed when protections fail. Bound chat inputs, concurrent turns, and cancellation-aware streaming queues.
2026-09-05 04:29:45 -04:00
Juan Denis 785c5780d3 fix(sockets): authorize subscriptions and revalidate event delivery
Require valid access-token sessions, scope application and server rooms, and enforce run visibility consistently across sockets and polling. Add positive-delivery and cross-user isolation regressions.
2026-09-05 04:29:44 -04:00
Juan Denis c8c93a8e0c fix(auth): enforce MFA, scoped keys, and revocable sessions
Add session-family revocation and migration 097, require current credentials for password changes, constrain API keys, and require verified WebAuthn authentication. Update browser token handling and add boundary regressions. Existing JWTs require fresh login; restricted keys deny undeclared endpoints.
2026-09-05 04:29:16 -04:00
Rebertt RodriguesandJuan Denis a092ba1afe fix(dashboard): avoid content blockers on metrics history requests (#133)
Co-authored-by: Juan Denis <jhd3197@gmail.com>
2026-09-04 22:20:22 -04:00
Juan DenisandClaude Fable 5.1 c2f34bf136 feat(deploy-console): parameterised repo simulated deployment
Add a `repo` scenario to the simulated deployments so a demo run reads like a
real deploy of a named service: clone, validate, build, route, start and
health-check lines carry the app's own name, repository, branch, port and
health path. The simulate endpoint also accepts a `title` (shown verbatim by
the Deploy Console) and `params.pace` to slow the stream for recordings.

Used by the walkthrough videos to show the Deploy Console streaming live
inside the read-only demo image, which has no git or docker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SiGEPH8S231SMwFWTd76b6
2026-09-04 21:12:18 -04:00
Juan DenisandClaude Opus 5 1f92ae1c91 fix(connect): use the policy door, the privilege wrapper, and safe reads
Four things the guards and the review caught, all of them real:

- app/api/connect.py: GET /connect/status carried a bare @jwt_required(),
  which the jwt-only census forbids for a new route (609 vs a ceiling of
  608). It is an admin read of pairing state, so it takes @admin_required
  — the same door the rest of the admin surface uses, API-key capable by
  way of auth_required() like its neighbours.
- connect_policy._systemctl ran systemctl through raw subprocess, one over
  the raw-subprocess ceiling. run_privileged() adds sudo when the panel is
  not root, so the fail2ban repair now works off a packaged install
  instead of reporting a permission error nobody can act on.
- connect_client.hello_headers assumed the end-to-end key fields were
  always there. build_hello() drops them rather than fail the connection,
  so a panel that could not read its X25519 key would KeyError on the
  long-poll fallback — exactly the path taken when things are already bad.
- status() called resolve_cloud_url(), which raises on a URL with no
  scheme. A typo in SERVERKIT_CLOUD_URL should surface when you pair, not
  turn `serverkit connect status` and the Settings panel into an error.

Both connect threads are now classified in the thread-ownership registry:
the relay loop is a process loop, and a signed command is bounded delivery
whose record lives in the control plane.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018q56AfqpgKjio4n9KYubvZ
2026-09-03 00:19:27 -04:00
Juan DenisandClaude Opus 5 8fe469e1fe feat(connect): let ServerKit Cloud set where this server backs up
The panel can now run the signed commands Cloud sends, and the first four
of them are the Storage Hub's. A destination assigned in Cloud is tested
here before it is saved, written as `s3.path_prefix` (which every upload,
list and verify path already honoured), and reported back on a `storage`
stream. Unassigning hands it back and deletes nothing in the bucket.

- backend/app/services/connect_commands.py — verify a command JWT against
  ServerKit Cloud's JWKS (signature, audience, exp, nonce, consent scope), run the
  handler on its own thread, report running/succeeded/failed. An action
  this version does not know fails with a sentence saying so.
- backend/app/services/connect_storage.py — storage.assign/unassign/
  test/report, and the snapshot the storage stream carries.
- backend/app/services/connect_client.py — handle `command` opens, fetch
  the JWKS per connection, publish a storage snapshot on connect and
  every five minutes.
- backend/app/api/backups.py — GET /storage says when Cloud owns the
  destination; PUT answers 409 rather than letting two writers race.
- frontend/src/pages/Backups.jsx — the local form is read-only while
  Cloud manages it, with the sentence that says how to take it back.
- backend/tests/test_connect_storage.py — 23 tests, no network.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018q56AfqpgKjio4n9KYubvZ
2026-09-02 15:19:38 -04:00
Juan Denis 0a654e0f5f feat(connect): pair the panel with ServerKit Cloud from the CLI
- app/services/connect_keys.py: Ed25519 device keypair (0600, config dir),
  fingerprint = sha256(raw pubkey)[:16] shown in groups of four
- app/services/connect_client.py: enroll/poll (signed PoP)/rotate against
  the control plane, writes connect.json (0600) on claim;
  status()/disconnect(); control-plane URL from --cloud / SERVERKIT_CLOUD_URL / https://app.serverkit.ai
- cli.py: 'connect' group (pair, status, disconnect)
- app/api/connect.py + core_blueprints.py: GET /api/v1/connect/status (JWT)
- tests/test_core_blueprint_registry.py: registry guard constants for the
  new blueprint
2026-09-02 09:26:42 -04:00
Juan DenisandClaude Opus 5 aa9a570f57 fix(apps): keep compose the default; divert only build-pack apps
The previous commit asked "does a compose file exist on disk?" and sent
everything else down the container path. That was too wide in both directions:

  - test_start_app_authenticates_registry_before_compose_up seeds a
    registry-bound app whose compose file has not been rendered yet, and
    rightly expects `docker login` -> `compose up` -> `docker logout`. The
    disk probe diverted it and the start 400'd.
  - it also silently changed behaviour for any compose app whose file was
    missing for an unrelated reason, turning a clear compose error into a
    confusing "not been deployed yet".

Invert it. `_is_single_container_app()` is a positive test: a local app, no
compose file recorded, none on disk, and `buildpack_type` set -- i.e. one the
build pack deployed as a single container. Compose stays the default for
everything else, so no existing path changes.

Also seed the new test through `make_application` rather than constructing
Application(...) by hand, which is what test_seed_one_door_ratchet enforces
for files added after 2026-08-19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 01:44:35 -04:00
Juan DenisandClaude Opus 5 503dedd95c fix(apps): start/stop/restart a docker app that has no compose project
`app_type == 'docker'` was being read as "compose-managed", but the build-pack
path deploys by building an image and running a single container
(DeploymentService._deploy_docker, named serverkit-app-<id>) and never writes
a compose file. start/stop/restart called `docker compose` anyway, which -- in
a directory with nothing to read -- fails with:

    no configuration file provided: not found

So every build-pack app returned 400 from the panel's start, stop and restart
buttons and could not be run at all. There was already a materialize-compose
guard for BYO-image apps, but it only fires when root_path is NULL; a
build-pack app has source on disk, so it fell straight through to compose.

Decide on the artifact rather than the type: `_has_local_compose_project()`
looks for a compose file on disk (the app's own `compose_file`, else the four
default names). When there is one, nothing changes. When there isn't, drive
the container the deploy actually created.

Stop treats an already-gone container as stopped -- reporting that as a
failure would strand the app in `running` forever. Start and restart explain
that the app has not been deployed yet rather than surfacing a docker error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 01:34:30 -04:00
Juan Denis cfcf965e24 feat: add declarative walkthrough contribution contract
Extensions can now contribute walkthroughs via contributions.walkthroughs
in plugin.json. The backend validates definitions against a shared
contract (route, signal, manual, and check completion types), transports
them through the dynamic contribution envelope, and persists custom
walkthroughs from the Studio. Includes the JSON Schema, SDK version gate,
and extension scaffold support.
2026-08-31 23:14:29 -04:00
Juan DenisandClaude Fable 5 cc0597bf28 refactor(admin): move the owned-apps delete guard behind the service door
The user-delete guard 79a78c3c added queried Application straight from
the controller, tripping the API controller boundary ratchet. The count
now lives in application_restore.applications_owned_by — the module
that owns Application's recycle-bin semantics and documents why
tombstoned rows block too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 21:04:21 -04:00
Juan DenisandClaude Fable 5 f34969eaac fix(models): close all 26 dangling-on-delete FKs + orphan sweep migration
Every NOT NULL FK from the KNOWN_DANGLING_ON_DELETE audit now declares
its parent-side relationship, so the delete-cascade policy door covers
it: AI conversations/pending actions, API keys, dashboards, event
subscriptions, login links, sent invitations, resource grants (users);
env vars + history, previews + settings, WAF policy (applications);
surveys, doctor results, proxy stack, tunnels (servers); manifests
(projects); DDNS hosts (zones); exposed services (tunnels); drills
(backup policies); rollouts (agent versions); site plugin installs
(WordPress sites); queue messages (groups). The ratchet set is now
EMPTY and stays that way.

One new deliberate exception: Workspace.projects — projects carry
environments and app groupings, so delete_workspace now refuses (409)
while any remain instead of silently destroying them (previously it
crashed with IntegrityError).

Migration 095 sweeps rows already orphaned on existing installs by
pre-fix hard-deletes — including an application's leftover env-var
secrets, which a reused SQLite row id could have attached to a future
app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:25:29 -04:00
Juan DenisandClaude Fable 5 79a78c3c38 fix(models): repo-wide delete-cascade audit for NOT NULL child FKs
Follow-up to 1704d60b, prompted by 'can this happen elsewhere?' — a
mapper-wide audit found 22 more one-to-many relationships whose child
FK is NOT NULL but which lacked a delete cascade, so hard-deleting the
parent (user/server/workspace/template/plugin delete, deployment
retention pruning, WP site teardown) raised the same IntegrityError as
the Recycle Bin purge.

20 get the cascade: plugin installs, deployment diffs, metric alerts,
template assignments (both sides), WP activities/sync/promotion jobs
(both sides), workspace members/keys, and the per-user detail rows
(passkeys, OAuth identities, notification prefs, sanitization
profiles, source connections, workflows, workspace memberships).

2 stay uncascaded on purpose, now enforced: User.applications (delete_
user refuses with 409 while the user owns apps — apps must go through
app delete + Recycle Bin purge so containers/volumes are cleaned) and
CloudProvider.servers (no delete path exists; dropping VM inventory
must stay a deliberate decision). The invariant sweep is now global
across every mapper with exactly that allowlist, which may only shrink.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:47:32 -04:00
Juan DenisandClaude Fable 5 160bf6dab7 feat(panel): one-click self-update from the About tab
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>
2026-08-31 08:09:52 -04:00