// POST action=logs name= last 200 lines, timestamped // POST action=pull_rebuild name= spawns the worker, returns job_id // POST action=job_status job_id= poll a pull_rebuild job // // RESPONSE // {"ok":bool,"output":""} start / stop / restart // {"ok":true,"logs":"…"} logs // {"ok":true,"status":"started","job_id":"…"} pull_rebuild // {"ok":true,"status":"pending"} or the worker's job file verbatim // {"ok":false,"error":"invalid name"|"invalid job_id"|"Container not found" // |"Could not determine image"|"Unknown action"} // // DEPENDS ON // include/config.php vv_cache_clear() // api/docker_pull_worker.php detached worker for the pull_rebuild path // docker CLI ps, logs, start, stop, inspect // dynamix.docker.manager rebuild_container, when present // ═══════════════════════════════════════════════════════════════════════════════════════════════ header('Content-Type: application/json'); // This file was self-contained until the cache invalidation below needed a shared helper, and the // call went in without the include. The fatal landed *after* docker stop had already run, so the // container went down and the page was told the action failed — the worst shape a bug here can // take, because the operator's next move is to try again on a machine that already did it. require_once dirname(__DIR__) . '/include/config.php'; define('VV_JOB_DIR', '/tmp/varaverk_dk_jobs'); $action = trim($_POST['action'] ?? ''); $name = trim($_POST['name'] ?? ''); $jobId = trim($_POST['job_id'] ?? ''); // ── Job status (no name required) ───────────────────────────────────────────── if ($action === 'job_status') { if (!$jobId || !preg_match('/^[0-9a-f]+$/', $jobId)) { echo json_encode(['ok' => false, 'error' => 'invalid job_id']); exit; } $file = VV_JOB_DIR . '/' . $jobId . '.json'; if (!file_exists($file)) { echo json_encode(['ok' => true, 'status' => 'pending']); exit; } echo file_get_contents($file); exit; } // ── Logs ────────────────────────────────────────────────────────────────────── if ($action === 'logs') { if (!$name || !preg_match('/^[a-zA-Z0-9_.-]+$/', $name)) { echo json_encode(['ok' => false, 'error' => 'invalid name']); exit; } $out = shell_exec('docker logs --tail 200 --timestamps ' . escapeshellarg($name) . ' 2>&1'); echo json_encode(['ok' => true, 'logs' => $out ?? '']); exit; } // ── Container-scoped actions ────────────────────────────────────────────────── if (!$name || !preg_match('/^[a-zA-Z0-9_.-]+$/', $name)) { echo json_encode(['ok' => false, 'error' => 'invalid name']); exit; } $check = trim(shell_exec( 'docker ps -a --filter ' . escapeshellarg('name=^' . $name . '$') . " --format '{{.Names}}' 2>/dev/null" ) ?? ''); if ($check !== $name) { 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') { 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; } if ($action === 'restart') { $rebuild = '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container'; if (is_executable($rebuild)) { exec(escapeshellarg($rebuild) . ' ' . escapeshellarg($name) . ' 2>&1', $out, $rc); } else { exec('docker stop ' . escapeshellarg($name) . ' 2>&1', $o1, $rc1); exec('docker start ' . escapeshellarg($name) . ' 2>&1', $o2, $rc2); $out = array_merge($o1, $o2); $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; } if ($action === 'pull_rebuild') { @mkdir(VV_JOB_DIR, 0700, true); $jobId = bin2hex(random_bytes(8)); $jobFile = VV_JOB_DIR . '/' . $jobId . '.json'; $worker = __DIR__ . '/docker_pull_worker.php'; $rebuild = '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container'; $oldId = trim(shell_exec('docker inspect --format={{.Image}} ' . escapeshellarg($name) . ' 2>/dev/null') ?: ''); $image = trim(shell_exec('docker inspect --format={{.Config.Image}} ' . escapeshellarg($name) . ' 2>/dev/null') ?: ''); if (!$image) { echo json_encode(['ok' => false, 'error' => 'Could not determine image']); exit; } file_put_contents($jobFile, json_encode(['ok' => true, 'status' => 'pulling', 'container' => $name])); $cmd = 'php ' . escapeshellarg($worker) . ' ' . escapeshellarg($name) . ' ' . escapeshellarg($jobFile) . ' ' . escapeshellarg($oldId) . ' ' . escapeshellarg($image) . ' ' . escapeshellarg($rebuild) . ' >/dev/null 2>&1 &'; exec($cmd); echo json_encode(['ok' => true, 'status' => 'started', 'job_id' => $jobId]); exit; } echo json_encode(['ok' => false, 'error' => 'Unknown action']);