feat: per-container Restart + Pull-latest buttons (unlock-gated)#15
Merged
Conversation
Each container row (local + remote host cards) gets Restart and Pull-latest buttons. Pull is compose-aware: it pulls and recreates the service via the container's compose labels, falling back to pull + restart for plain `docker run` containers. Also adds a compact uptime/age badge per container. Security: new POST /api/container-action is login + shell-unlock + action-header gated (same as the tmux session controls), and the UI hides the buttons until shells are unlocked + confirms each action. Container names and engines are strictly validated (no shell metacharacters) before they touch a shell; remote commands run over SSH stdin, not as command-line args. Verified against logan-gl502vs: unauthenticated action → 403; unlocked restart of glances bounced it (StartedAt advanced); cargo test (31) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds per-container lifecycle controls (Restart + Pull latest) to the dashboard for both local and remote container lists, plus a compact uptime/age badge derived from container status. It introduces a new backend endpoint to execute these actions with the same login + shell-unlock + action-header gating model already used for tmux session controls.
Changes:
- Added
POST /api/container-action(login + unlock + action-header gated) to run restart/pull actions locally or via SSH on configured remote hosts. - Implemented container action execution logic (
src/container_actions.rs) with engine/name validation and time-bounded command execution. - Updated UI rendering to show per-container uptime badges and action buttons, wiring click events to the new API and hiding mutating controls unless shells are unlocked.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes.rs | Adds the new /api/container-action endpoint and routes requests to local/remote action runners with existing guard/unlock/action-header gates. |
| src/main.rs | Registers the new container_actions module. |
| src/container_actions.rs | Implements validated local/remote restart + pull flows and adds unit tests for validation/script construction. |
| public/metrics.js | Adds uptime badge parsing, action button HTML, and client-side handler to call the new endpoint. |
| public/events.js | Wires click delegation for container action buttons to containerAction(...). |
| public/app.css | Styles the uptime badge and action buttons; hides actions while shells are locked. |
| frontend/metrics.ts | TypeScript source for uptime badge + action controls and API calls (mirrored into public/metrics.js). |
| frontend/events.ts | TypeScript source for click delegation to containerAction(...) (mirrored into public/events.js). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+32
to
+45
| const PULL_TEMPLATE: &str = r#"set -e | ||
| ENG=__ENGINE__ | ||
| NAME=__NAME__ | ||
| proj=$($ENG inspect -f '{{ index .Config.Labels "com.docker.compose.project.config_files" }}' "$NAME" 2>/dev/null || true) | ||
| svc=$($ENG inspect -f '{{ index .Config.Labels "com.docker.compose.service" }}' "$NAME" 2>/dev/null || true) | ||
| dir=$($ENG inspect -f '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' "$NAME" 2>/dev/null || true) | ||
| img=$($ENG inspect -f '{{ .Config.Image }}' "$NAME" 2>/dev/null || true) | ||
| if [ -n "$proj" ] && [ -n "$svc" ]; then | ||
| [ -n "$dir" ] && cd "$dir" 2>/dev/null || true | ||
| $ENG compose -f "$proj" pull "$svc" && $ENG compose -f "$proj" up -d "$svc" && echo "pulled + recreated $NAME ($svc)" | ||
| else | ||
| $ENG pull "$img" && $ENG restart "$NAME" && echo "pulled $img + restarted $NAME (not compose-managed)" | ||
| fi | ||
| "#; |
Comment on lines
+141
to
+146
| let mut child = command | ||
| .spawn() | ||
| .map_err(|_| "Could not start SSH".to_string())?; | ||
| if let Some(mut stdin) = child.stdin.take() { | ||
| let _ = stdin.write_all(script.as_bytes()).await; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds dashboard lifecycle controls for containers, plus a per-container uptime badge.
What
pull + restartfor plaindocker runcontainers.7d,14h) parsed from the status.Security (mutating action on live containers)
POST /api/container-actionis login + shell-unlock + action-header gated — identical to the existing tmux session controls.Verified against logan-gl502vs
403 {"error":"Shell unlock required"}.glancesactually bounced it (StartedAt advanced, "Up 3 seconds").cargo test(31 pass),tscclean, screenshot of the buttons (clean compact layout).Companion ops change (not in this PR)
gl502vs containers that had no RAM limit were capped live via
docker update(generous ceilings above current usage) so a memory-leaking container gets OOM-killed at its own limit instead of taking the host down.🤖 Generated with Claude Code