Commit Graph
11 Commits
Author SHA1 Message Date
menzeljandClaude Opus 4.8 98d756faf6 0.37.4: fix folder upload doing nothing (set webkitdirectory reliably)
"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>
2026-06-21 20:52:50 +00:00
menzeljandClaude Opus 4.8 a43e6b48f0 0.37.3: byte-accurate upload progress + file counter for folder uploads
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>
2026-06-21 20:41:42 +00:00
menzeljandClaude Opus 4.8 5ac9f15de4 0.37.2: stream folder zip-downloads to fix 504 on large folders
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>
2026-06-21 20:28:49 +00:00
menzeljandClaude Opus 4.8 0dc430bb2a 0.37.1: fix folder-download hang/501 on special files + add download progress
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>
2026-06-21 20:11:50 +00:00
menzeljandClaude Opus 4.8 f1782eca0e 0.37.0: download whole folders (recursive) as a .zip from the file browser
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>
2026-06-21 19:54:50 +00:00
menzeljandClaude Opus 4.8 5bcec06bbd 0.36.1: app logo as browser-tab favicon
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>
2026-06-16 21:24:07 +00:00
menzeljandClaude Opus 4.8 cd15cdc75e 0.36.0: per-stack image-update indicator on the Stacks overview
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>
2026-06-16 18:52:51 +00:00
menzeljandClaude Opus 4.8 a4b1bbcdd1 0.35.0: per-stack Update button on the Stacks page
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>
2026-06-16 18:44:21 +00:00
menzeljandClaude Fable 5 786c346c40 0.33.0: NFS share as backup destination
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>
2026-06-12 12:39:35 +00:00
menzeljandClaude Fable 5 79d82361d8 0.32.1: backup/restore fixes (audit findings, all paths live-verified)
- 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>
2026-06-12 12:10:45 +00:00
menzeljandClaude Fable 5 a0dda120f5 0.32.0: StackPilot self-update (check on page load + one-click update)
- 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>
2026-06-12 09:01:08 +00:00