Shows an amber "Update" pill next to a stack's status (and highlights the
inline Update button) when any of the stack's images has a newer digest in
the registry. Reuses the existing background image-update check — a new
update_service.stacks_update_summary() reads the cached digests in a single
container sweep (no extra registry calls), exposed as GET /api/stacks/updates
and proxied per agent at GET /api/agents/{id}/stacks/updates. The Stacks page
and each remote-host section poll it every 60s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New destination type 'nfs' alongside SFTP and S3. The StackPilot container
needs no mount privileges: the Docker daemon mounts the export as a named
volume (stackpilot-nfs-dest-<id>, driver local/type nfs, recreated whenever
server/path/options change) and all file I/O runs through throwaway helper
containers (BACKUP_HELPER_IMAGE) — upload via put_archive, list via stat,
download via get_archive, delete/test via short-lived runs. Config: server,
export path, mount options (default rw), optional subdirectory (sanitized;
shell-safe charset). Mount failures surface as clean destination errors.
Settings UI gains the NFS form + summary; works everywhere destinations are
used (push, restore-from, scheduled backups incl. retention).
Verified live against a real kernel NFS server: test, push (file on the
export), list, restore-from incl. volume data, remote delete, config change
recreates the mount volume, unreachable server fails cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- backup_filename() crashed with NameError (bare now()) since 0.8.0 —
broke every scheduled backup at the upload step, agent backup download
and the central remote-backup/push endpoints. The local manual path
worked only because the router had its own copy (now an alias).
- restore: the manifest stack_id from an uploaded backup is now slugified
too — a crafted '../../...' id could previously escape STACKS_DIR.
- create_backup no longer starts a previously-stopped stack (stop/restart
only when the stack was actually running).
- overwrite-restore wipes the existing volume contents before extracting,
so files created since the backup no longer survive underneath it.
Verified end-to-end: full/config backup contents (compose, .env, .secrets,
bind dirs, extras, volume tars), delete→restore round-trip incl. volume
data, rename restore with volume re-prefixing, 409 conflict + overwrite,
traversal guard, scheduled run + retention prune + restore-from against
real MinIO, and the complete remote-agent cycle (download/push/restore).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- backend/version.py is now the single version source (main.py, agent).
- GET /api/system/update: reads the version tags of the backend's own image
repo (anonymous v2 token flow, https→http fallback for insecure
registries), compares the highest semver tag against APP_VERSION; reports
update_supported from the container's compose labels. 10 min cache.
- POST /api/system/update (admin, audited): spawns a detached helper
container from the current backend image that runs docker compose pull &&
up -d on StackPilot's own compose project (project name, working dir and
config files resolved from its own container labels) — the helper
outlives the backend being recreated. Non-compose installs get a 400.
- /api/health now returns the version so the UI can detect the switchover.
- TopNav version badge: queries the update status on page load; when a
newer release exists an amber pill shows the version — one click (admin)
confirms, triggers the update and overlays a wait screen that polls
/api/health and reloads once the new version answers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Container card now compares compose-only counts across hosts: agents
report compose_running in /agent/system (pre-0.31.1 agents fall back to
the all-containers number); card retitled, ResourceBar stat labelled
'Containers (all)'.
- Uptime is sampled every 5 min (background loop + opportunistic on read)
and charted as daily averages instead of a once-a-day snapshot; no
sample is written when no compose containers exist (was: fake 100%).
Legacy daily entries in uptime.jsonl still count; file pruned at startup.
- Funnel stage 'monitored' is now per-stack and real: stacks with an
enabled local auto-update policy (was: global webhook-exists toggle).
Frontend label renamed to 'Auto-managed'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Templates are now stack-shaped folders (compose.yaml + .env.example +
template.json) instead of DB rows + manifest.json + {{VAR}} rendering.
Pull copies the folder into a new stack; custom templates persist under
DATA_DIR/templates. Adds POST /api/templates/from-stack and a one-time
startup migration for pre-0.31 DB templates (drops the template table).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Manage Docker secrets and configs per stack from a new Secrets tab on Stack/
RemoteStackDetail. Content is stored as files inside the stack dir
(.secrets/<name>, .configs/<name>; dir 0700 / file 0600) and referenced from the
compose file with relative `file:` paths, so the daemon reads them without any
HOST_ROOT_PREFIX dependency. Content is write-only — the API only ever returns
metadata (name, kind, size).
- secret_service: write/delete/list (metadata only)/exists/rel_path/attach/detach;
name validation rejects traversal/hidden/separators, content capped at 1 MiB.
- compose_edit_service: add/remove secret and config (top-level defs pruned when
no service still references them).
- routers/secrets.py (admin-only, audit secret.*) + agent endpoints + multi-host
proxy (audit agent.secret.*).
- Frontend SecretsPanel (create/list/delete + per-row attach/detach to a service;
config rows take a mount target), agentId-aware for remote stacks.
Verified: name-sandbox + perms + metadata-only listing unit-tested; compose
add/remove round-trips to clean YAML; py_compile + backend/agent/frontend image
builds + route smoke-test (local/agent/proxy). Live exec check (/run/secrets/<name>
on a deployed stack) and swarm path are hardware-verify debt (swarm dropped: A).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-stack auto-update policy on the stack Overview tab. When the background
image-update check finds a newer registry digest for one of a stack's images,
the stack is pulled + redeployed (or just flagged, "notify only"). Only running
stacks are auto-redeployed; a stopped stack is skipped, never silently started.
- models/auto_update.py: AutoUpdate(stack_id, agent_id, enabled, redeploy,
last_run/status/result) + schemas; registered in models/__init__.py.
- update_service: DB-free stack_images/stack_updates helpers (agent reuses
them); agent GET /agent/stacks/{id}/updates.
- services/auto_update_service.py: run_due/run_policy (local pull+up via
compose_service, remote via agent_service POST /agent/stacks/{id}/update,
notify-only with per-transition dedup); lazy-called from
update_service.background_loop. New stack_auto_updated notify event.
- routers: GET/PUT/run /api/stacks/{id}/auto-update and the
/api/agents/{id}/stacks/{sid}/auto-update variants (policy stored centrally).
- frontend: api/autoUpdate.ts + AutoUpdatePanel (enable, redeploy|notify-only,
Check now, last-run status) on StackDetail + RemoteStackDetail; EVENT_LABELS
gains stack_auto_updated + backup_failed.
Live-verified all four paths (updated / update-available / up-to-date /
skipped) against real compose.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Interactive shell into a compose-managed container over WebSocket + xterm.js,
opened from the container card on the stack Overview tab. Admin-only (non-admin
handshake rejected with 4403); only containers with the compose project label
are reachable.
- backend services/exec_service.py: create/start/resize exec + a shared
bidirectional pump_exec (recv/sendall on sock._sock, executor thread,
resize control frames, exit-code frame).
- routers/ws.py: _authorize_admin + /ws/exec/{container_id} and the
/ws/agent-exec/{agent_id}/{container_id} proxy (forwards BOTH directions).
- agent_app.py: /agent/ws/exec/{container_id}.
- frontend: @xterm/xterm + @xterm/addon-fit; ContainerTerminal modal (shell
picker, fit/resize, exit/error handling) + a Terminal button on ContainerCard.
Live-verified (TestClient): local happy/exit/guard/4403/4401, agent happy/4401,
proxy bidirectional round-trip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stack Overview now renders each service as an expandable ContainerCard with a
curated single-container inspect view and admin start/stop/restart buttons,
both for local stacks (GET/POST /api/containers/{id}[/{action}]) and remote
stacks (proxied via /api/agents/{id}/containers/* to the agent's new
/agent/containers/* endpoints). Only compose-managed containers are exposed.
Also bumps version 0.23.0 -> 0.26.0 (the bumps for the already-committed
Phase 18 image-prune / Phase 19 compose-validate were missed) and backfills
README sections for Phase 18/19/20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deploying a local stack from the editor now opens a console modal that streams
the `docker compose up -d` output (image pulls, container creation) live over a
new `/ws/deploy/{stack_id}` WebSocket, replacing the blind "Deploying…" spinner.
The compose subprocess keeps running server-side if the modal is closed early;
the same audit entry + start/error notification as the REST start path is recorded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stacks overview and detail were doing an N+1 inspect storm: list_stacks
called containers_for_stack AND compute_status (which re-fetched) per
stack, and containers.list(sparse=False) full-inspects every container
plus c.image triggered an image-inspect each. For N stacks that was
~2N*(1 list + M inspects + M image-inspects) sequential socket round
trips (~1s for just 2 stacks, growing linearly).
- compose_service.stack_status_summaries(): one low-level
api.containers(all=True) summary call grouped by compose project label
→ whole list served in a single Docker round-trip (~10x faster).
- compute_status() takes optional pre-fetched containers; get_stack and
_stack_summary no longer double-fetch.
- containers_for_stack() reads the image name from the inspect it already
has instead of c.image (drops the per-container image-inspect).
- Same batching applied to the agent's stack list/detail.
Also: Forgejo (registry + git) moved to 10.10.6.10:3020 — updated image
refs in docker-compose.yml, agent/Dockerfile, agent/docker-compose.yml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port-conflict check now matches the stack's own containers via the
compose project label instead of a fragile container-name prefix, so
editing + deploying a running stack no longer reports false conflicts
(explicit container_name or '_' name separator).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Docker's volume list has no size, so add a "Compute sizes" button that runs
`docker system df` (via client.df()) and shows per-volume size in a new Size
column. The df walk is expensive (seconds), so results are cached ~60s and
loaded on demand instead of on every poll.
- volume_service.volume_sizes(force) with a 60s TTL cache; GET /api/volumes/sizes
+ agent /agent/volumes/sizes + proxy /api/agents/{id}/volumes/sizes.
- Frontend: volumesApi.sizes(force, agentId); Volumes page gained a Size column
and a Compute sizes button (per host) that triggers the lookup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard now lists stacks in a table with live CPU and memory usage per
stack. Usage is sampled from docker stats (one-shot read per running container,
using the daemon-provided precpu for the CPU delta) and aggregated by compose
project.
- services/stats_service.py + GET /api/stacks/stats: per-stack cpu_used (cores),
mem_used (bytes minus reclaimable cache), and the summed assigned cpu/mem
limits (null when none set), read concurrently across containers.
- Dashboard: stacks render as a table with a CPU and a Memory meter. When a
limit is assigned the bar fills toward it (used / limit + %); otherwise it
fills toward the host total. Inline start/stop/restart per row for admins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Networks and Images are now per-host, rendered as a section for the local host
plus one per registered agent (like the Stacks page).
- agent_app.py: new /agent/networks (list/inspect/containers/connect/disconnect/
create/delete/prune) and /agent/images (list/updates/check), reusing
network_service and a new image_service; DockerError mapped to HTTP status
(forbidden -> 400 so the proxy doesn't treat it as a token failure).
- routers/agents.py: proxy routes at /api/agents/{id}/networks/* and
/api/agents/{id}/images/*, audit-logging mutations.
- services/image_service.py: extracted the image-listing logic so the central
router and the agent share it.
- Frontend: networksApi/imagesApi take an optional agentId; Networks/Images
pages render NetworksSection/ImagesSection per host with a shared HostHeader.
Remote "Prune unused" networks resolves the address-pool-exhaustion deploy
error from the UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Folder upload: the Files page gained an "Upload folder" picker
(webkitdirectory); each file is sent with its webkitRelativePath and the
backend recreates the directory tree. upload_target now accepts an optional
rel_path, creating intermediate dirs (mkdir -p) inside the sandbox with each
component validated against traversal.
Copy/move: new file_service.copy/move + POST /api/files/{copy,move}
(admin, audit-logged). The UI adds per-row copy/cut actions, a clipboard bar
to paste into the current directory, and an overwrite prompt on conflict.
Both refuse to move/copy a folder into itself or its own subtree and are
sandbox-checked on source and destination.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a full host filesystem browser reachable from the sidebar (/files):
breadcrumb navigation, browse-root chips, show-hidden toggle, and a table
with size/permissions/mtime. Text files open in a Monaco editor (language by
extension); binary/oversized files fall back to download. Admins can create
folders/files, rename, delete (recursive for dirs), upload, and save edits;
download is available to all users. Every mutation is audit-logged.
Backend: new services/file_service.py reuses device_service's sandbox helpers
(confined to ALLOWED_BROWSE_ROOTS, mapped via HOST_ROOT_PREFIX) and rejects
path traversal and deleting a browse root. routers/files.py exposes
/api/files/{list,read,download,write,mkdir,touch,rename,upload,DELETE}
(reads: any user; mutations: admin). device_service.browse entries gained
mtime + symlink (non-breaking).
Deployment: ALLOWED_BROWSE_ROOTS + HOST_ROOT_PREFIX are now env-wired in
docker-compose.yml and .env.example, with a commented /:/host_root mount to
browse/manage the real host filesystem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Live remote-stack logs over a WebSocket proxied through the central app to
the agent (/ws/agent-logs/{agent}/{stack}); agent gains a WS log endpoint.
- Deploy to a remote host from the UI: host selector in the New Stack editor
and template dialog; templates instantiate onto an agent via the proxy.
- Network attach/detach: expandable inspect view per network with
connect/disconnect + container picker; GET /{id}/containers, POST connect/disconnect.
- Remove dead pages/Placeholder.tsx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- gpu_service: detect host render/video group GIDs from /dev/dri node ownership
(render node → render GID, paired card node → video GID); added to GPUInfo +
exposed via /api/system/gpus. inject_dri now emits numeric group_add entries
(e.g. ["991","44"]) when GIDs are known, falling back to names otherwise;
remove_gpu strips those GIDs + LIBVA_DRIVER_NAME; dri_group_gids() for cleanup.
- editor set-gpu passes render_gid/video_gid through; GPUSelector shows detected
GIDs, defaults video group on, and sends them.
Verified: py_compile, unit check (inject→["991","44"] then clean removal),
frontend tsc build, image imports. Live iGPU verify is on the user's hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Networks: network_service (list w/ subnet/containers/in-use/owning-stack,
create bridge/macvlan/ipvlan/overlay + optional subnet/gateway/internal,
delete with default-network guard, prune) + routers/networks.py; real
Networks page replaces the placeholder.
- Fix: local stacks can now be deleted from the UI — Delete button on stack
detail (with optional keep-files-on-disk) and a trash action on stack cards,
via a shared ConfirmDialog. (Backend DELETE existed; no UI surfaced it.)
Verified: py_compile, frontend tsc build, live network list smoke test
(defaults flagged, compose nets + in-use detected); main 104 routes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- BackupSchedule model + schedule_service: next-run calc (hourly/daily/weekly,
UTC), background scheduler loop (lifespan), run-one with retention pruning
(keep newest N per stack on the destination), backup_failed notify event.
- routers/schedules.py: schedules CRUD + run-now; registered in main.py.
- Frontend: api/schedules.ts + Settings → Scheduled backups (list with next/last
run + status, enable/disable, run-now, delete; add form with stack/destination/
frequency/time/weekday/retention/volumes).
Rough-verified only (per request): py_compile, frontend tsc build, app import
(95 routes), next-run math sanity. Full live run to be tested after deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- BackupDestination model + backup_destination_service (SFTP via paramiko,
S3-compatible via boto3): upload/list/download/delete/test.
- routers/destinations.py: destinations CRUD (secrets masked, merge-on-update),
test, list/delete remote backups. backups.py: POST /{id}/backup/push and
POST /restore-from (download from a destination + restore, volumes included).
- Frontend: Settings → Backup destinations (SFTP/S3 forms + test); Backup dialog
can push to a destination; Restore dialog can pick a destination + backup.
- deps: paramiko 3.5.0, boto3 1.35.99.
Verified end-to-end against live MinIO + atmoz/sftp: create/test destinations,
push (incl. volumes), list, restore-from to a fresh stack (volume data intact),
delete remote backup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>