Commit Graph
23 Commits
Author SHA1 Message Date
menzeljandClaude Opus 5 adfd77a983 Fix NFS uploads against root_squash exports (0.40.2)
Uploading a backup extracted the tar straight into the NFS mount via
put_archive, and the daemon chowns every entry while extracting — an export
with root_squash refuses that ("failed to Lchown ... for UID 0, GID 0:
operation not permitted"), so the upload died with a docker 500 even though
plain writes to the share work (which is why the destination test passed).

The helper container now unpacks into its own filesystem and copies the file
into the mount with cat, which never chowns. Restores hit the same wall when a
volume or bind folder lives on a squashed mount, so import_path/import_volume
fall back to a copy-through-staging when (and only when) the failure is a chown
denial — local restores keep preserving ownership. NFS file names are validated
against the same safe charset as the subdir parts, since both are interpolated
into the helper's shell commands.

Verified against a real root_squash NFS export: test/upload/list/download/delete
round trip, byte-identical download, restore into an NFS-backed volume via the
fallback, and ownership still preserved (1000:1000, 0600) on a local volume.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:53:25 +00:00
menzeljandClaude Opus 5 4c158e9407 Fix NFS backup destinations broken by the 0.40.0 refactor (0.40.1)
_ensure_helper_image moved to stack_assets_service, but backup_destination_service
imports it lazily inside _nfs_run/_nfs_helper, so nothing failed at import time —
every NFS destination operation raised ImportError at runtime instead. The helper
is now a public ensure_helper_image() and the NFS helpers import it from its new
home. Verified: every services/ and routers/ module imports, and both NFS helper
paths run through to a Docker call instead of ImportError.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:29:10 +00:00
menzeljandClaude Opus 5 5347a36eaf Back up bind-mount data, not just the compose file (0.40.0)
A stack's real state lives in its bind-mounted config directories, and those
were never captured: the backup only tarred the stack folder as this container
sees it. When STACKS_HOST_DIR differs from the container's STACKS_DIR, compose
resolves ./config against the container path and the daemon creates it at that
path on the *host* — invisible here, so the archive held little more than
compose.yaml and .env.

New services/stack_assets_service.py inventories a stack's data (bind sources
merged from container mounts + the compose file, named volumes) and does all
data I/O through a throwaway helper container, i.e. by host path, so unseen
directories are captured anyway. It also detects the host/container stacks-path
mismatch and reports it.

- manifest v2: full inventory, per-asset capture result, skip reasons (v1 still
  restores)
- NFS/CIFS-backed volumes are skipped by default and never wiped on restore
- deselected data inside the stack folder no longer sneaks in via compose/
- volume/bind archives stream through temp files instead of RAM
- restore preserves mode, ownership, mtime and symlinks, and writes bind folders
  back to their host paths (rewritten when the stack is renamed)
- backup dialog shows the inventory with sizes and per-item checkboxes; restore
  gained a "restore bind folders" toggle
- new GET /api/stacks/{id}/backup/inventory (+ agent + proxy), backup endpoints
  take include_binds/binds/volumes, restore takes restore_binds

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:20:19 +00:00
menzeljandClaude Opus 5 ecf780c5e6 Deploy console: real progress bar for image pulls (0.39.0)
Compose is now run with `--progress json` (probed once, falls back to the
plain text stream on older compose/agents). The console folds the event
stream into a weighted progress bar — download bytes per layer, then
container create/start — with a per-image bar and a byte/layer counter,
and renders the raw output one line per layer (updated in place) instead
of a wall of scrolling text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 14:19:27 +00:00
menzeljandClaude Fable 5 9119f94536 Clear the stack update pill immediately after a manual/auto update (0.38.4)
The amber image-update indicator is fed from update_service._CACHE, which
only the background loop refreshed — after a per-stack Update/Pull the
stale digests kept the pill on until the next pass. Now the local digests
are reconciled with the cached remote digests right after a successful
pull/update (local backend, agent lifecycle, auto-update pass), and the
frontend invalidates the stack-updates queries after actions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:52:31 +00:00
menzeljandClaude Opus 4.8 e651029ab2 db: auto-add missing model columns on startup (fix backupschedule.agent_id) (0.38.3)
create_all never ALTERs an existing table, so installs predating the
backupschedule.agent_id column kept the old schema and any ORM query
naming it failed with "no such column" — which the new fleet dashboard
(and the schedules list / scheduler loop) hit. init_db now diffs each
mapped table against the live schema and ADD COLUMNs the missing
nullable/defaulted ones. Idempotent and self-healing for similar drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:43:17 +00:00
menzeljandClaude Opus 4.8 d399caadc9 Dashboard: surface the real compute_fleet error in the response (0.38.2)
The fleet endpoint returned a bare 500, so the error banner only showed
"status code 500" with no cause. Wrap the call to log the full traceback
server-side and return the exception type, message and originating
file:line in the HTTP detail, so the dashboard banner pinpoints the
failure for an authenticated user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:31:19 +00:00
menzeljandClaude Opus 4.8 0b95d7d4a2 Dashboard: surface fleet load errors instead of infinite skeletons (0.38.1)
The cockpit cards gated purely on `fleet.data`, so any failed
/api/dashboard/fleet request (e.g. a stale backend returning 404, or a
500) left the new components stuck on skeletons forever. Render a clear
error banner with the API message and a Retry button when the query
errors with no data, so the actual cause is visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:17:32 +00:00
menzeljandClaude Opus 4.8 c830d28b65 Dashboard: rebuild into an operator cockpit (0.38.0)
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>
2026-06-24 11:04:57 +00:00
menzeljandClaude Opus 4.8 5c46e40866 0.37.7: surface folder-upload diagnostics (find why it does nothing)
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>
2026-06-21 21:30:46 +00:00
menzeljandClaude Opus 4.8 415ebb733a 0.37.6: remember the "Show hidden" toggle across reloads
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>
2026-06-21 21:26:03 +00:00
menzeljandClaude Opus 4.8 a40dd0de3e 0.37.5: overwrite prompt for file upload + auto-reveal uploaded hidden files
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>
2026-06-21 21:20:15 +00:00
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