* feat(updates): auto-prune dangling images after updates
Each update pulls a fresh image and recreates containers, leaving the
replaced image behind as a dangling layer that previously had to be
pruned by hand. A new "Prune dangling images after updates" toggle under
Settings > System > Docker hygiene reclaims these automatically.
The setting is on by default and opt-out. When enabled, a successful
stack update (manual or scheduled) and a Sencho self-update each remove
the dangling image layers they orphaned. Only untagged layers are
touched; tagged images, volumes, and data are never removed. The toggle
requires an admin account and is per node: each instance honors its own
value, so a remote node self-update applies that node's own preference.
A prune failure never affects the update result: on the stack path it is
caught and logged after the update has already succeeded, and on the
self-update path the helper-shell prune runs only after a clean recreate
and cannot change the exit code or the recorded update error.
* security(self-update): shell-quote label-derived values in helper command
Address review feedback on the prune-on-update change:
- The self-update helper command interpolated the compose service name and
config-file paths (both read from Docker Compose labels) straight into a
shell string. Shell-quote them via shQuote so a label carrying shell
metacharacters stays inert data and cannot break the exit-code capture,
error-file write, or prune guard.
- Correct the settings copy and docs: the prune is a standard dangling-image
prune, so it reclaims every untagged layer on the node, not only the one the
current update orphaned. Tagged images, volumes, and data remain untouched.
- Add tests: shell-metacharacter neutralization and prune-output suppression in
the self-update command, and an atomic-update case asserting a prune failure
does not trigger a rollback.
* fix(updates): omit the reclaim figure when the daemon reports zero bytes
End-to-end testing on a Docker daemon backed by the containerd image store
showed the post-update prune removing a dangling image while the prune API
returned SpaceReclaimed=0, so the stream printed "reclaimed 0.0 MB" even though
an image was removed. Show the reclaimed figure only when the daemon reports a
non-zero value; otherwise the line reads "=== Pruned dangling images ===". The
overlay2 store still reports real figures and shows them. Add a test covering
both branches.
SelfUpdateService scanned the container's Mounts list for Type='bind'
only when picking the host path to forward to the helper container.
A pilot enrolled with the recommended Docker Compose snippet uses a
named volume (sencho-agent-data:/app/data, Type='volume'), so the
lookup returned null and the boot log read "/app/data mount not found
- update error recovery will be unavailable". The helper still ran
on update but could not persist UPDATE_ERROR_FILE on a failed pull,
so the next gateway process had nothing to surface.
Extract findDataDirHost(mounts) and accept Type='bind' or
Type='volume'. Docker populates Source with the on-disk volume
directory for either type, so the existing :rw bind in the helper
spawn works unchanged. The independent hostBindMounts filter stays
strict to Type='bind' (forwarded operator-declared compose paths
only; named volumes are not in scope there).
Tests: pure unit coverage for the helper across 6 cases (bind hit,
volume hit, no /app/data, mixed list with siblings, empty Source,
tmpfs ignored).