diff --git a/public/assets/js/pi_control_console.js b/modules/pi_control/assets/console.js similarity index 100% rename from public/assets/js/pi_control_console.js rename to modules/pi_control/assets/console.js diff --git a/modules/pi_control/assets/pi_control.css b/modules/pi_control/assets/pi_control.css new file mode 100644 index 0000000..6032d47 --- /dev/null +++ b/modules/pi_control/assets/pi_control.css @@ -0,0 +1,74 @@ +.form-card { padding: 14px; } +.form-grid { display: grid; gap: 12px; } +.form-field { display: grid; gap: 6px; } +.form-field input, +.form-field select, +.form-field textarea { width: 100%; } + +.icon-button { + border: 1px solid var(--line); + background: var(--panel-2); + border-radius: 999px; + padding: 4px 10px; + cursor: pointer; + font-weight: 700; +} +.icon-button:hover { background: var(--panel); } + +.console-tabs { + border: 1px solid var(--line); + border-radius: 14px; + overflow: hidden; + background: #0b0f17; +} +.console-tab-bar { + display: flex; + gap: 6px; + flex-wrap: wrap; + padding: 8px; + background: #0f1624; + border-bottom: 1px solid rgba(255,255,255,0.08); +} +.console-tab { + background: transparent; + color: #c9d3e3; + border: 1px solid transparent; + border-radius: 10px; + padding: 6px 10px; + cursor: pointer; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 8px; +} +.console-tab.is-active { + border-color: rgba(255,255,255,0.2); + background: rgba(255,255,255,0.08); + color: #ffffff; +} +.console-tab-panels { + min-height: 420px; +} +.console-tab-close { + display: inline-flex; + width: 18px; + height: 18px; + align-items: center; + justify-content: center; + border-radius: 999px; + background: rgba(255,255,255,0.12); + color: #ffffff; + font-size: 0.8rem; + line-height: 1; +} +.console-tab-close:hover { + background: rgba(255,255,255,0.25); +} +.console-panel { display: none; } +.console-panel.is-active { display: block; } +.console-panel iframe { + width: 100%; + height: 520px; + border: 0; + background: #0b0f17; +} diff --git a/modules/pi_control/bootstrap.php b/modules/pi_control/bootstrap.php index 1a384e8..1a4c698 100644 --- a/modules/pi_control/bootstrap.php +++ b/modules/pi_control/bootstrap.php @@ -94,6 +94,7 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName token VARCHAR(64) NOT NULL UNIQUE, host_id INTEGER NOT NULL, provider VARCHAR(20) NOT NULL DEFAULT 'ttyd', + command_text TEXT NULL, created_by VARCHAR(120) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, @@ -139,6 +140,7 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName token VARCHAR(64) NOT NULL UNIQUE, host_id INTEGER NOT NULL, provider VARCHAR(20) NOT NULL DEFAULT 'ttyd', + command_text TEXT NULL, created_by VARCHAR(120) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, @@ -154,6 +156,7 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName $pdo->exec("ALTER TABLE {$runTable} ADD COLUMN IF NOT EXISTS timeout_sec INTEGER NULL"); $pdo->exec("ALTER TABLE {$runTable} ADD COLUMN IF NOT EXISTS started_at TIMESTAMP NULL"); $pdo->exec("ALTER TABLE {$runTable} ADD COLUMN IF NOT EXISTS finished_at TIMESTAMP NULL"); + $pdo->exec("ALTER TABLE {$sessionTable} ADD COLUMN IF NOT EXISTS command_text TEXT NULL"); } else { $columns = []; $stmt = $pdo->query('PRAGMA table_info(' . $cmdTable . ')'); @@ -184,6 +187,15 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName if (empty($columns['finished_at'])) { $pdo->exec("ALTER TABLE {$runTable} ADD COLUMN finished_at DATETIME NULL"); } + + $columns = []; + $stmt = $pdo->query('PRAGMA table_info(' . $sessionTable . ')'); + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $col) { + $columns[$col['name']] = true; + } + if (empty($columns['command_text'])) { + $pdo->exec("ALTER TABLE {$sessionTable} ADD COLUMN command_text TEXT NULL"); + } } // Seed default commands (only when empty) diff --git a/modules/pi_control/pages/asset.php b/modules/pi_control/pages/asset.php new file mode 100644 index 0000000..34f8c68 --- /dev/null +++ b/modules/pi_control/pages/asset.php @@ -0,0 +1,30 @@ + $base . '/pi_control.css', + 'console.js' => $base . '/console.js', +]; + +if (!isset($map[$file])) { + http_response_code(404); + exit('Not found'); +} + +$path = $map[$file]; +if (!$base || !is_file($path) || !str_starts_with($path, $base)) { + http_response_code(404); + exit('Not found'); +} + +$ext = pathinfo($path, PATHINFO_EXTENSION); +if ($ext === 'css') { + header('Content-Type: text/css; charset=utf-8'); +} elseif ($ext === 'js') { + header('Content-Type: application/javascript; charset=utf-8'); +} else { + header('Content-Type: application/octet-stream'); +} + +readfile($path); +exit; diff --git a/modules/pi_control/pages/commands.php b/modules/pi_control/pages/commands.php index bc78928..9fadc18 100644 --- a/modules/pi_control/pages/commands.php +++ b/modules/pi_control/pages/commands.php @@ -2,6 +2,10 @@ $pdo = module_fn('pi_control', 'pdo'); module_fn('pi_control', 'ensure_schema'); $table = fn(string $name) => module_fn('pi_control', 'table', $name); +$assets = app()->assets(); +if ($assets) { + $assets->addStyle('/module/pi_control/asset?file=pi_control.css'); +} $notice = null; $error = null; diff --git a/modules/pi_control/pages/console.php b/modules/pi_control/pages/console.php index 0971ae1..559e800 100644 --- a/modules/pi_control/pages/console.php +++ b/modules/pi_control/pages/console.php @@ -14,7 +14,8 @@ $terminalHostLabel = null; $settings = modules()->settings('pi_control'); $assets = app()->assets(); if ($assets) { - $assets->addScript('/assets/js/pi_control_console.js', 'footer', true); + $assets->addStyle('/module/pi_control/asset?file=pi_control.css'); + $assets->addScript('/module/pi_control/asset?file=console.js', 'footer', true); } $ttydUrl = trim((string)($settings['ttyd_url'] ?? '/ttyd')); $defaultProvider = 'ttyd'; @@ -105,22 +106,20 @@ if (isset($_GET['open_console_json'])) { $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 . ')' + 'INSERT INTO ' . $table('sessions') . ' (token, host_id, provider, command_text, created_by, expires_at) + VALUES (:token, :host_id, :provider, :command_text, :created_by, ' . $expiresSql . ')' ); + $commandToRun = $presetCommand !== '' ? $presetCommand : $rawCommand; $stmt->execute([ 'token' => $token, 'host_id' => $hostId, 'provider' => 'ttyd', + 'command_text' => $commandToRun !== '' ? $commandToRun : null, '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']); @@ -281,7 +280,7 @@ $runs = $pdo->query(