Make a container action tell the truth about what happened
The response was discarded, so a refused stop looked like a completed one, and the payload the card redraws from is cached for 300s with no invalidation — the container carried on showing as running until the once-a-minute writer caught up. Stop now confirms; start still does not.
This commit is contained in:
@@ -127,8 +127,15 @@ if ($check !== $name) {
|
|||||||
echo json_encode(['ok' => false, 'error' => 'Container not found']); exit;
|
echo json_encode(['ok' => false, 'error' => 'Container not found']); exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every arm below drops the monitor cache once the container state has actually changed. That
|
||||||
|
// payload carries the container list the dashboard draws, is served with a 300s window, and is
|
||||||
|
// otherwise only rewritten by the once-a-minute cache writer — so without this the operator stops
|
||||||
|
// a container, watches the card, and sees it running for up to a minute with no hint as to why.
|
||||||
|
// Cleared after the command rather than before, so a failed action does not throw away a payload
|
||||||
|
// that is still accurate.
|
||||||
if ($action === 'start' || $action === 'stop') {
|
if ($action === 'start' || $action === 'stop') {
|
||||||
exec(($action === 'start' ? 'docker start' : 'docker stop') . ' ' . escapeshellarg($name) . ' 2>&1', $out, $rc);
|
exec(($action === 'start' ? 'docker start' : 'docker stop') . ' ' . escapeshellarg($name) . ' 2>&1', $out, $rc);
|
||||||
|
if ($rc === 0) vv_cache_clear('monitor');
|
||||||
echo json_encode(['ok' => $rc === 0, 'output' => implode("\n", $out)]); exit;
|
echo json_encode(['ok' => $rc === 0, 'output' => implode("\n", $out)]); exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +149,7 @@ if ($action === 'restart') {
|
|||||||
$out = array_merge($o1, $o2);
|
$out = array_merge($o1, $o2);
|
||||||
$rc = ($rc1 === 0 && $rc2 === 0) ? 0 : 1;
|
$rc = ($rc1 === 0 && $rc2 === 0) ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
if ($rc === 0) vv_cache_clear('monitor');
|
||||||
echo json_encode(['ok' => $rc === 0, 'output' => implode("\n", $out)]); exit;
|
echo json_encode(['ok' => $rc === 0, 'output' => implode("\n", $out)]); exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ if (PHP_SAPI !== 'cli') {
|
|||||||
|
|
||||||
if (!$name || !$jobFile || !$image) exit(1);
|
if (!$name || !$jobFile || !$image) exit(1);
|
||||||
|
|
||||||
|
// Only for vv_cache_clear() below. Required after the CLI guard, so a stray web request is turned
|
||||||
|
// away before this process loads anything at all.
|
||||||
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
|
||||||
function jw(string $f, array $d): void { file_put_contents($f, json_encode($d)); }
|
function jw(string $f, array $d): void { file_put_contents($f, json_encode($d)); }
|
||||||
|
|
||||||
shell_exec('docker pull ' . escapeshellarg($image) . ' 2>&1');
|
shell_exec('docker pull ' . escapeshellarg($image) . ' 2>&1');
|
||||||
@@ -115,6 +119,10 @@ if ($rebuild && is_executable($rebuild)) {
|
|||||||
$rc = ($rc1 === 0 && $rc2 === 0) ? 0 : 1;
|
$rc = ($rc1 === 0 && $rc2 === 0) ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The container was stopped and started to get here whether or not the rebuild reported success,
|
||||||
|
// so the cached container list is out of date either way.
|
||||||
|
vv_cache_clear('monitor');
|
||||||
|
|
||||||
jw($jobFile, $rc === 0
|
jw($jobFile, $rc === 0
|
||||||
? ['ok' => true, 'status' => 'done', 'updated' => true, 'message' => 'Updated and rebuilt']
|
? ['ok' => true, 'status' => 'done', 'updated' => true, 'message' => 'Updated and rebuilt']
|
||||||
: ['ok' => false, 'status' => 'done', 'error' => 'Rebuild failed after pull']
|
: ['ok' => false, 'status' => 'done', 'error' => 'Rebuild failed after pull']
|
||||||
|
|||||||
@@ -456,6 +456,17 @@ function vv_cache_write(string $key, array $data): void {
|
|||||||
rename($tmp, $f);
|
rename($tmp, $f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop a cached payload so the next read collects fresh. For use by endpoints that change the
|
||||||
|
// very state a cache describes: without it the UI polls a payload that cannot yet know about the
|
||||||
|
// action it just took, and the operator sees a container they stopped still running until the
|
||||||
|
// background writer next comes round.
|
||||||
|
//
|
||||||
|
// Best-effort by design. A cache that could not be removed is a stale read, which is what would
|
||||||
|
// have happened anyway — never a reason to fail the action that was actually requested.
|
||||||
|
function vv_cache_clear(string $key): void {
|
||||||
|
@unlink(VV_CACHE_DIR . '/' . $key . '.json');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Shared utility functions (used across include/ and api/ files) ────────────
|
// ── Shared utility functions (used across include/ and api/ files) ────────────
|
||||||
|
|
||||||
// Format seconds into "2d 3h 15m".
|
// Format seconds into "2d 3h 15m".
|
||||||
|
|||||||
@@ -27,8 +27,15 @@
|
|||||||
// the page reported healthy unconditionally (fixed 2026-08-02). If this panel looks
|
// the page reported healthy unconditionally (fixed 2026-08-02). If this panel looks
|
||||||
// suspiciously green, verify the paths before believing it.
|
// suspiciously green, verify the paths before believing it.
|
||||||
//
|
//
|
||||||
// Container actions are confirmed and routed through the action endpoint, which validates
|
// Stopping a container is confirmed; starting one is not, and the endpoint validates every
|
||||||
// against real inventory.
|
// action against real inventory regardless. Failures are surfaced rather than swallowed — the
|
||||||
|
// response used to be discarded, which made a refused stop indistinguishable from a completed
|
||||||
|
// one.
|
||||||
|
//
|
||||||
|
// An action that changes container state clears the monitor cache.
|
||||||
|
// api/docker_action.php and the pull worker both drop it, so the next poll collects
|
||||||
|
// instead of re-reading a payload written before the action. Without that the card
|
||||||
|
// contradicted the button for up to a minute.
|
||||||
//
|
//
|
||||||
// All remote and container-supplied strings render escaped.
|
// All remote and container-supplied strings render escaped.
|
||||||
// Through vvEscHtml()/vvEscAttr() from Varaverk.page — media titles and usernames from
|
// Through vvEscHtml()/vvEscAttr() from Varaverk.page — media titles and usernames from
|
||||||
@@ -2249,13 +2256,27 @@ function vvDockerAction(action, name, webui) {
|
|||||||
encodeURIComponent('/boot/config/plugins/dockerMan/templates-user/my-' + name + '.xml') + '&update=true';
|
encodeURIComponent('/boot/config/plugins/dockerMan/templates-user/my-' + name + '.xml') + '&update=true';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Stopping is confirmed; starting is not. The asymmetry is the point — start is recoverable by
|
||||||
|
// clicking the other button, stop takes a service away from whoever is using it, and these
|
||||||
|
// buttons sit inside a dense grid where the row under the cursor is easy to misjudge.
|
||||||
|
if (action === 'stop' && !confirm('Stop ' + name + '?')) return;
|
||||||
|
|
||||||
const fd = new URLSearchParams();
|
const fd = new URLSearchParams();
|
||||||
fd.set('action', action);
|
fd.set('action', action);
|
||||||
fd.set('name', name);
|
fd.set('name', name);
|
||||||
fetch('/plugins/varaverk/api/docker_action.php', { method: 'POST', body: fd })
|
fetch('/plugins/varaverk/api/docker_action.php', { method: 'POST', body: fd })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(() => { vvDfActive = null; setTimeout(vvPollMonitor, 1500); })
|
.then(d => {
|
||||||
.catch(() => {});
|
// The endpoint reports refusals as ok:false with a reason — container not found, a non-zero
|
||||||
|
// docker exit. Discarding that made a failed stop look exactly like a successful one, since
|
||||||
|
// the card it would have changed is redrawn from a payload either way.
|
||||||
|
if (!d || !d.ok) alert('Container ' + action + ' failed: ' + ((d && (d.error || d.output)) || 'unknown error'));
|
||||||
|
vvDfActive = null;
|
||||||
|
// The endpoint drops the monitor cache on success, so this poll collects fresh rather than
|
||||||
|
// re-reading the payload that was written before the action happened.
|
||||||
|
setTimeout(vvPollMonitor, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => alert('Container ' + action + ' failed: request error'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function vvRenderDockerFolders(data) {
|
function vvRenderDockerFolders(data) {
|
||||||
|
|||||||
Reference in New Issue
Block a user