module_fn('pi_control', 'table', $name); $notice = null; $error = null; $terminalNotice = null; $terminalError = null; $terminalUrl = null; $terminalToken = null; $terminalHostLabel = null; $settings = modules()->settings('pi_control'); $assets = app()->assets(); if ($assets) { $assets->addScript('/assets/js/pi_control_console.js', 'footer', true); } $ttydUrl = trim((string)($settings['ttyd_url'] ?? '/ttyd')); $defaultProvider = 'ttyd'; $defaultTimeout = (int)($settings['exec_default_timeout'] ?? (getenv('PI_CONTROL_EXEC_DEFAULT_TIMEOUT') !== false ? (int)getenv('PI_CONTROL_EXEC_DEFAULT_TIMEOUT') : 300)); $defaultTimeout = $defaultTimeout > 0 ? $defaultTimeout : 300; $queueName = (string)($settings['redis']['queue'] ?? ($settings['redis.queue'] ?? (getenv('PI_CONTROL_REDIS_QUEUE') ?: 'pi_control:queue'))); $tokenTtl = (int)($settings['terminal_token_ttl'] ?? 10); $tokenTtl = $tokenTtl > 0 ? $tokenTtl : 10; $hosts = $pdo->query('SELECT * FROM ' . $table('hosts') . ' ORDER BY name ASC')->fetchAll(PDO::FETCH_ASSOC); $commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY label ASC')->fetchAll(PDO::FETCH_ASSOC); $renderRuns = function (array $runs): string { ob_start(); if (!$runs) { echo 'Noch keine Runs.'; return ob_get_clean(); } foreach ($runs as $r) { $out = (string)($r['output'] ?? ''); $err = (string)($r['error'] ?? ''); $snippet = $out !== '' ? $out : $err; if (strlen($snippet) > 140) { $snippet = substr($snippet, 0, 140) . '…'; } echo ''; echo '' . e((string)$r['id']) . ''; echo '' . e((string)($r['status'] ?? '')) . ''; echo '' . e((string)($r['host_name'] ?? $r['host_addr'] ?? '')) . ''; echo '' . e((string)($r['command_text'] ?? '')) . ''; echo '' . e((string)($r['created_by'] ?? '')) . ''; echo '' . e($snippet) . ''; echo '' . (!empty($r['timeout_sec']) ? e((string)$r['timeout_sec']) . 's' : 'default') . ''; echo ''; } return ob_get_clean(); }; if (isset($_GET['queue_json'])) { $runs = $pdo->query( 'SELECT r.*, h.name AS host_name, h.host AS host_addr FROM ' . $table('runs') . ' r LEFT JOIN ' . $table('hosts') . ' h ON h.id = r.host_id ORDER BY r.id DESC LIMIT 20' )->fetchAll(PDO::FETCH_ASSOC); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'ok' => true, 'next' => 10, 'html' => $renderRuns($runs), ]); exit; } if (isset($_GET['open_console_json'])) { $hostId = (int)($_POST['terminal_host_id'] ?? 0); $presetId = (int)($_POST['terminal_preset_id'] ?? 0); $rawCommand = trim((string)($_POST['terminal_command_text'] ?? '')); $terminalError = null; $terminalUrl = null; $terminalToken = null; $terminalHostLabel = null; if ($hostId <= 0) { $terminalError = 'Bitte einen Host wählen.'; } elseif ($ttydUrl === '') { $terminalError = 'ttyd URL fehlt. Bitte im Setup setzen.'; } else { $presetCommand = ''; if ($presetId > 0) { foreach ($commands as $c) { if ((int)$c['id'] === $presetId) { if (!auth_is_admin() && !empty($c['admin_only'])) { $terminalError = 'Diese Vorlage ist nur für Admins.'; } else { $presetCommand = (string)$c['command']; } break; } } } if (!$terminalError) { $driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME); $expiresSql = $driver === 'pgsql' ? "NOW() + INTERVAL '{$tokenTtl} minutes'" : "DATETIME('now', '+{$tokenTtl} minutes')"; $token = bin2hex(random_bytes(24)); $terminalToken = $token; $stmt = $pdo->prepare( 'INSERT INTO ' . $table('sessions') . ' (token, host_id, provider, created_by, expires_at) VALUES (:token, :host_id, :provider, :created_by, ' . $expiresSql . ')' ); $stmt->execute([ 'token' => $token, 'host_id' => $hostId, 'provider' => 'ttyd', 'created_by' => auth_display_name() ?: null, ]); $sep = str_contains($ttydUrl, '?') ? '&' : '?'; $terminalUrl = rtrim($ttydUrl, '/') . '/' . $sep . 'arg=' . rawurlencode($token); $commandToRun = $presetCommand !== '' ? $presetCommand : $rawCommand; if ($commandToRun !== '') { $terminalUrl .= '&arg=' . rawurlencode(base64_encode($commandToRun)); } foreach ($hosts as $h) { if ((int)$h['id'] === $hostId) { $terminalHostLabel = (string)($h['name'] ?? $h['host']); break; } } } } header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'ok' => $terminalError === null, 'error' => $terminalError, 'url' => $terminalUrl, 'token' => $terminalToken, 'host' => $terminalHostLabel, ]); exit; } if (isset($_GET['run_command_json'])) { $hostId = (int)($_POST['terminal_host_id'] ?? 0); $commandId = (int)($_POST['terminal_preset_id'] ?? 0); $rawCommand = trim((string)($_POST['terminal_command_text'] ?? '')); $timeoutSec = $defaultTimeout; $error = null; $notice = null; if ($hostId <= 0) { $error = 'Bitte einen Host wählen.'; } elseif ($commandId <= 0 && $rawCommand === '') { $error = 'Bitte einen Befehl wählen oder einen Befehl eingeben.'; } else { $selectedCommand = ''; if ($commandId > 0) { foreach ($commands as $c) { if ((int)$c['id'] === $commandId) { if (!auth_is_admin() && !empty($c['admin_only'])) { $error = 'Dieser Befehl ist nur für Admins.'; } else { $selectedCommand = (string)$c['command']; if (!empty($c['timeout_sec'])) { $timeoutSec = (int)$c['timeout_sec']; } } break; } } } if (!$error) { $commandText = $selectedCommand !== '' ? $selectedCommand : $rawCommand; $driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME); if ($driver === 'pgsql') { $stmt = $pdo->prepare( 'INSERT INTO ' . $table('runs') . ' (host_id, command_id, command_text, status, timeout_sec, created_by) VALUES (:host_id, :command_id, :command_text, :status, :timeout_sec, :created_by) RETURNING id' ); $stmt->execute([ 'host_id' => $hostId, 'command_id' => $commandId > 0 ? $commandId : null, 'command_text' => $commandText, 'status' => 'queued', 'timeout_sec' => $timeoutSec, 'created_by' => auth_display_name() ?: null, ]); $runId = (int)$stmt->fetchColumn(); } else { $stmt = $pdo->prepare( 'INSERT INTO ' . $table('runs') . ' (host_id, command_id, command_text, status, timeout_sec, created_by) VALUES (:host_id, :command_id, :command_text, :status, :timeout_sec, :created_by)' ); $stmt->execute([ 'host_id' => $hostId, 'command_id' => $commandId > 0 ? $commandId : null, 'command_text' => $commandText, 'status' => 'queued', 'timeout_sec' => $timeoutSec, 'created_by' => auth_display_name() ?: null, ]); $runId = (int)$pdo->lastInsertId(); } try { $redis = module_fn('pi_control', 'redis'); $payload = json_encode(['run_id' => $runId], JSON_THROW_ON_ERROR); $redis->command(['RPUSH', $queueName, $payload]); $notice = 'Befehl wurde in die Queue gestellt.'; } catch (\Throwable $e) { $pdo->exec('UPDATE ' . $table('runs') . ' SET status = \'queue_error\' WHERE id = ' . (int)$runId); $notice = 'Befehl gespeichert, aber Queue nicht erreichbar.'; } } } header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'ok' => $error === null, 'error' => $error, 'notice' => $notice, ]); exit; } // Form submits are handled via AJAX to avoid reloads. $runs = $pdo->query( 'SELECT r.*, h.name AS host_name, h.host AS host_addr FROM ' . $table('runs') . ' r LEFT JOIN ' . $table('hosts') . ' h ON h.id = r.host_id ORDER BY r.id DESC LIMIT 20' )->fetchAll(PDO::FETCH_ASSOC); ?>
Pi Control

Konsole

Wähle einen Host und führe einen Befehl aus.

Live-Konsole
Token:
Konsole Tabs

Mehrere Konsolen bleiben hier parallel offen.

Queue
Update in 10s
ID Status Host Command Von Output Timeout