Skip to content

feat: per-container Restart + Pull-latest buttons (unlock-gated)#15

Merged
falkoro merged 1 commit into
masterfrom
feat/container-actions
May 31, 2026
Merged

feat: per-container Restart + Pull-latest buttons (unlock-gated)#15
falkoro merged 1 commit into
masterfrom
feat/container-actions

Conversation

@falkoro
Copy link
Copy Markdown
Owner

@falkoro falkoro commented May 31, 2026

Adds dashboard lifecycle controls for containers, plus a per-container uptime badge.

What

  • Restart and Pull-latest buttons on every container row (local and remote host cards).
  • Pull is compose-aware: pulls + recreates the service via the container's compose labels; falls back to pull + restart for plain docker run containers.
  • Compact uptime/age badge per container (e.g. 7d, 14h) parsed from the status.

Security (mutating action on live containers)

  • New POST /api/container-action is login + shell-unlock + action-header gated — identical to the existing tmux session controls.
  • UI hides the buttons until shells are unlocked and confirms each action.
  • Container names + engines are strictly validated (charset has no shell metacharacters) before touching a shell; remote commands run over SSH stdin, never as command-line args.

Verified against logan-gl502vs

  • Unauthenticated action → 403 {"error":"Shell unlock required"}.
  • Unlocked restart of glances actually bounced it (StartedAt advanced, "Up 3 seconds").
  • cargo test (31 pass), tsc clean, 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

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>
Copilot AI review requested due to automatic review settings May 31, 2026 12:26
@falkoro falkoro merged commit 8f5d266 into master May 31, 2026
@falkoro falkoro deleted the feat/container-actions branch May 31, 2026 12:26
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thread src/container_actions.rs
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 thread src/container_actions.rs
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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants