Replace the analytics-style dashboard (stack-health funnel, uptime %,
operations/day grid, AI pill) with an attention-driven fleet cockpit:
- New /api/dashboard/fleet endpoint: server-side fan-out across the local
host and every agent into one payload — a prioritized "needs attention"
list, headline KPIs, an honest stack-status breakdown and a per-host
resource rollup. Each agent uses its own DB session so the fan-out is
concurrency-safe; failures degrade to "offline" instead of stalling.
- New frontend: AttentionStrip, FleetKpiRow, StackStatusBar and
HostResourceTable; Dashboard.tsx rewritten around them.
- Remove the funnel/summary endpoints, the uptime sampler loop and the
ops-activity machinery; delete the now-unused chart components.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Folder upload still reported as doing nothing, and without browser access
the failure point is invisible. Make every outcome visible on-screen:
- onChange: if the folder picker returns 0 files, toast an error; otherwise
toast "Starting folder upload: N file(s)…" so it's clear the upload fired
(independent of the progress bar rendering).
- Per-file failures are no longer swallowed: capture the first error and
show it in the result toast ("Uploaded X, Y failed — <path>: <reason>").
This pinpoints whether the picker returns nothing, the upload never starts,
or the requests fail (and why). Frontend-only; all 3 images pushed 0.37.7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uploaded dotfiles (.env) were persisting fine, but "Show hidden" is
component state that reset to off on every reload — so after refreshing,
hidden files disappeared from view and looked lost. Persist the toggle in
localStorage (sp.files.showHidden) so it survives reloads; combined with
0.37.5's auto-reveal, an uploaded .env now stays visible.
Frontend-only; all 3 images rebuilt+pushed 0.37.6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reported "upload doesn't work, file never appears" was two things, both
hit when uploading config files like .env:
1. Single-file upload used overwrite=false and dead-ended on "Already
exists: .env — rename or remove the existing file first." with no way to
replace the file. Now a conflict opens an Overwrite confirmation dialog
(mirroring the copy/paste conflict flow) that retries with overwrite=true.
2. .env (and any dotfile) is hidden, so even a successful upload stayed
invisible unless "Show hidden" was on. After an upload whose name/path
has a dot-segment, "Show hidden" is now auto-enabled so the file shows.
The single-file upload mutation now takes {file, overwrite}; folder upload
(already overwrite=true) also auto-reveals hidden results.
Frontend-only; all 3 images rebuilt+pushed 0.37.5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Upload folder" silently did nothing: the directory-selection attribute
was set on the hidden <input> via a JSX spread
({...{webkitdirectory:"", directory:""}}), which React doesn't reliably
apply to the DOM — and if isAdmin resolves after first render, a one-shot
effect would miss the input mounting entirely. Without the attribute the
picker is a plain file picker where no folder can be selected, so the user
picks nothing and nothing happens.
- Set webkitdirectory/directory/mozdirectory imperatively through a
callback ref, which runs whenever the input mounts. folderInput is now a
MutableRefObject so the callback can populate it.
- Folder upload now shows the progress bar immediately on start (small
files can finish before the browser emits any upload-progress event, so
don't wait for the first one to render feedback).
Frontend-only; all 3 images rebuilt+pushed 0.37.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed the upload path. Single-file and folder uploads already drove the
progress bar, but folder progress was file-COUNT based ((i + filePct)/total),
which jumps around when a folder mixes tiny files with large ones and gives
no sense of total size.
- Folder upload: progress is now byte-weighted (sum of all file sizes), so
the bar tracks real transfer. Added a detail line "<i> / <n> files ·
<sent> / <total>" and the bar shows the current file name.
- Single file: added the same byte detail ("<sent> / <size>").
- Progress component gained an optional detail sub-line (shared by the
download bar too).
Frontend-only; all 3 images rebuilt+pushed 0.37.3 for tag consistency.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A big folder hit a 504 Gateway Timeout: the zip was built into a temp
file *before* any response was sent, so for large folders the backend
stayed silent past nginx's proxy_read_timeout.
Now the zip is streamed as it's built, end to end:
- file_service.open_archive() returns (filename, byte iterator); _iter_zip
walks the dir and yields zip bytes incrementally via a small drain
buffer, writing each file in 1 MiB chunks (bounded memory, valid CRCs).
Same hardening as before — only real regular files; FIFOs/sockets/
devices/symlinks skipped without open(); per-file read errors skipped.
- /api/files/download and /agent/files/download return a StreamingResponse
(no temp file). The agent proxy streams the agent response straight
through (agent_service.stream_download), pulling the first chunk eagerly
so an offline/bad-token agent still yields a clean status before 200.
- Files page: streamed downloads have no Content-Length, so the progress
bar shows the running downloaded byte count ("Downloading … 12.3 MB")
instead of a percentage, after the initial "Preparing …".
Verified end to end via TestClient (200, application/zip, valid zip,
2 MiB file intact, FIFO skipped, no hang).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues with the 0.37.0 folder zip-download:
1. Hang / server error (reported as 501) on "some folders". archive_dir
tried to zip every entry, including non-regular files. Opening a FIFO
blocks forever (no writer); a unix socket / unreadable file raised an
OSError that aborted the whole archive. Now only real regular files are
zipped — FIFOs, sockets, devices and symlinks are skipped without ever
open()-ing them, and a per-file read error skips just that file instead
of failing the download.
2. No feedback while a large folder is being prepared. The zip is built
server-side before any bytes flow, so the click felt dead. The Files
page now shows an indeterminate "Preparing <name>…" bar from click,
switching to a real percentage during the transfer (Content-Length is
known for the finished zip). filesApi.download forwards onDownloadProgress.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The file browser/editor could only download individual files. Add a
recursive directory download that streams the folder as a zip archive,
on the local host and on every remote agent.
- file_service.archive_dir(): zip a directory recursively into a temp
file, preserving the folder name as the archive root and empty
subdirectories; symlinks are skipped (no sandbox escape / loops).
- /api/files/download and /agent/files/download branch on directories
and return application/zip, cleaning up the temp file afterwards.
- Files page: show the download button for folders too (as <name>.zip).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add frontend/public/favicon.svg (the TopNav LogoMark glyph as a
standalone SVG) and link it from index.html so the StackPilot logo
shows in browser tabs. Vite copies public/ into dist on build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Adds an inline "Update (pull latest images & recreate)" action to each
row of the stacks table, next to start/stop/restart/edit — for both the
local host and remote agents. Wires the existing updateImages action and
the agent "update" lifecycle action through StacksTable's new onUpdate prop.
Also bumps backend/version.py to 0.35.0 so it tracks the frontend version
again (it had drifted to 0.33.0 while package.json moved to 0.34.x).
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>