From 12c2ce48171473a85515588116ef11c8981509eb Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sat, 7 Mar 2026 23:58:40 +0100 Subject: [PATCH] console --- modules/pi_control/assets/commands.js | 97 ++++++++++ modules/pi_control/assets/hosts.js | 56 ++++++ modules/pi_control/assets/pi_control.css | 113 +++++++++++ modules/pi_control/bootstrap.php | 18 ++ modules/pi_control/module.json | 15 +- modules/pi_control/pages/asset.php | 2 + modules/pi_control/pages/commands.php | 144 ++++++++++---- modules/pi_control/pages/hosts.php | 229 ++++++++++++++++++----- partials/structure/layout_start.php | 42 ++++- public/assets/css/app.css | 32 +++- 10 files changed, 652 insertions(+), 96 deletions(-) create mode 100644 modules/pi_control/assets/commands.js create mode 100644 modules/pi_control/assets/hosts.js diff --git a/modules/pi_control/assets/commands.js b/modules/pi_control/assets/commands.js new file mode 100644 index 0000000..48c5cb0 --- /dev/null +++ b/modules/pi_control/assets/commands.js @@ -0,0 +1,97 @@ +(() => { + const form = document.querySelector('[data-command-form]'); + const list = document.querySelector('[data-command-list]'); + if (!form) return; + + const idInput = form.querySelector('input[name="id"]'); + const labelInput = form.querySelector('input[name="label"]'); + const commandInput = form.querySelector('textarea[name="command"]'); + const timeoutInput = form.querySelector('input[name="timeout_sec"]'); + const adminInput = form.querySelector('input[name="admin_only"]'); + const submitBtn = form.querySelector('[data-command-submit]'); + const cancelBtn = form.querySelector('[data-command-cancel]'); + + const resetForm = () => { + if (idInput) idInput.value = ''; + if (labelInput) labelInput.value = ''; + if (commandInput) commandInput.value = ''; + if (timeoutInput) timeoutInput.value = ''; + if (adminInput) adminInput.checked = false; + if (submitBtn) submitBtn.textContent = 'Speichern'; + }; + + document.querySelectorAll('[data-command-edit]').forEach((btn) => { + btn.addEventListener('click', () => { + const item = btn.closest('.command-item'); + if (!item) return; + if (idInput) idInput.value = item.dataset.commandId || ''; + if (labelInput) labelInput.value = item.dataset.label || ''; + if (commandInput) commandInput.value = item.dataset.command || ''; + if (timeoutInput) timeoutInput.value = item.dataset.timeout || ''; + if (adminInput) adminInput.checked = item.dataset.admin === '1'; + if (submitBtn) submitBtn.textContent = 'Aktualisieren'; + const details = btn.closest('details'); + if (details) details.removeAttribute('open'); + form.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + }); + + if (cancelBtn) { + cancelBtn.addEventListener('click', (e) => { + e.preventDefault(); + resetForm(); + }); + } + + if (!list) return; + + let dragging = null; + + list.querySelectorAll('.command-item').forEach((item) => { + item.addEventListener('dragstart', () => { + dragging = item; + item.classList.add('is-dragging'); + }); + item.addEventListener('dragend', () => { + item.classList.remove('is-dragging'); + dragging = null; + saveOrder(); + }); + }); + + list.addEventListener('dragover', (e) => { + e.preventDefault(); + if (!dragging) return; + const after = getDragAfterElement(list, e.clientY); + if (after == null) { + list.appendChild(dragging); + } else if (after !== dragging) { + list.insertBefore(dragging, after); + } + }); + + const getDragAfterElement = (container, y) => { + const elements = [...container.querySelectorAll('.command-item:not(.is-dragging)')]; + return elements.reduce( + (closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + if (offset < 0 && offset > closest.offset) { + return { offset, element: child }; + } + return closest; + }, + { offset: Number.NEGATIVE_INFINITY, element: null } + ).element; + }; + + const saveOrder = () => { + const order = [...list.querySelectorAll('.command-item')].map((el) => el.dataset.commandId); + fetch(window.location.pathname + '?reorder_json=1', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ order }), + cache: 'no-store', + }).catch(() => {}); + }; +})(); diff --git a/modules/pi_control/assets/hosts.js b/modules/pi_control/assets/hosts.js new file mode 100644 index 0000000..6f37319 --- /dev/null +++ b/modules/pi_control/assets/hosts.js @@ -0,0 +1,56 @@ +(() => { + const form = document.querySelector('[data-host-form]'); + if (!form) return; + + const idInput = form.querySelector('input[name="id"]'); + const nameInput = form.querySelector('input[name="name"]'); + const hostInput = form.querySelector('input[name="host"]'); + const portInput = form.querySelector('input[name="port"]'); + const userInput = form.querySelector('input[name="username"]'); + const authSelect = form.querySelector('select[name="auth_type"]'); + const keyInput = form.querySelector('input[name="key_path"]'); + const passInput = form.querySelector('input[name="password"]'); + const imageInput = form.querySelector('input[name="image_url"]'); + const submitBtn = form.querySelector('[data-host-submit]'); + const cancelBtn = form.querySelector('[data-host-cancel]'); + + const resetForm = () => { + if (idInput) idInput.value = ''; + if (nameInput) nameInput.value = ''; + if (hostInput) hostInput.value = ''; + if (portInput) portInput.value = '22'; + if (userInput) userInput.value = ''; + if (authSelect) authSelect.value = 'key'; + if (keyInput) keyInput.value = ''; + if (passInput) passInput.value = ''; + if (imageInput) imageInput.value = ''; + if (submitBtn) submitBtn.textContent = 'Speichern'; + }; + + document.querySelectorAll('[data-host-edit]').forEach((btn) => { + btn.addEventListener('click', () => { + const card = btn.closest('.host-card'); + if (!card) return; + if (idInput) idInput.value = card.dataset.hostId || ''; + if (nameInput) nameInput.value = card.dataset.name || ''; + if (hostInput) hostInput.value = card.dataset.host || ''; + if (portInput) portInput.value = card.dataset.port || '22'; + if (userInput) userInput.value = card.dataset.username || ''; + if (authSelect) authSelect.value = card.dataset.auth || 'key'; + if (keyInput) keyInput.value = card.dataset.keyPath || ''; + if (passInput) passInput.value = ''; + if (imageInput) imageInput.value = card.dataset.imageUrl || ''; + if (submitBtn) submitBtn.textContent = 'Aktualisieren'; + const details = btn.closest('details'); + if (details) details.removeAttribute('open'); + form.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + }); + + if (cancelBtn) { + cancelBtn.addEventListener('click', (e) => { + e.preventDefault(); + resetForm(); + }); + } +})(); diff --git a/modules/pi_control/assets/pi_control.css b/modules/pi_control/assets/pi_control.css index db34cd5..85169c8 100644 --- a/modules/pi_control/assets/pi_control.css +++ b/modules/pi_control/assets/pi_control.css @@ -73,6 +73,119 @@ background: #0b0f17; } +.host-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 16px; +} +.host-card { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 16px; + overflow: hidden; + display: grid; + grid-template-rows: 120px 1fr; + box-shadow: var(--shadow); +} +.host-card-image { + background: linear-gradient(135deg, #2b3a67 0%, #3b2f5c 45%, #1c2b3f 100%); + background-size: cover; + background-position: center; + position: relative; +} +.host-card-overlay { + position: absolute; + inset: 0; + background: rgba(10, 16, 28, 0.35); +} +.host-card-body { + padding: 12px 14px 14px; + display: grid; + gap: 6px; +} +.host-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.host-card-title { + display: inline-flex; + align-items: center; + gap: 8px; +} +.status-dot { + width: 10px; + height: 10px; + border-radius: 999px; + display: inline-block; +} +.status-ok { background: #31c48d; } +.status-auth { background: #fbbf24; } +.status-down { background: #ef4444; } + +.action-menu { + position: relative; +} +.action-menu summary { + list-style: none; + cursor: pointer; + border-radius: 10px; + padding: 2px 6px; + border: 1px solid var(--line); + background: var(--panel-2); +} +.action-menu summary::-webkit-details-marker { display: none; } +.action-menu[open] summary { + background: var(--panel); +} +.action-menu-panel { + position: absolute; + right: 0; + top: calc(100% + 6px); + background: var(--panel); + border: 1px solid var(--line); + border-radius: 12px; + padding: 6px; + min-width: 160px; + display: grid; + gap: 4px; + z-index: 5; + box-shadow: var(--shadow); +} +.action-menu-panel form { margin: 0; } + +.command-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 10px; +} +.command-item { + display: grid; + grid-template-columns: 28px 1fr auto; + gap: 10px; + align-items: start; + padding: 10px 12px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel); +} +.command-item.is-dragging { + opacity: 0.6; +} +.command-drag { + cursor: grab; + color: var(--muted); + font-size: 1.1rem; + padding-top: 4px; +} +.command-body code { + display: inline-block; + word-break: break-word; +} + .queue-button { display: inline-flex; align-items: center; diff --git a/modules/pi_control/bootstrap.php b/modules/pi_control/bootstrap.php index 1a4c698..1bafcdd 100644 --- a/modules/pi_control/bootstrap.php +++ b/modules/pi_control/bootstrap.php @@ -64,6 +64,7 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName auth_type VARCHAR(20) NOT NULL DEFAULT 'key', key_path TEXT NULL, password TEXT NULL, + image_url TEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS {$cmdTable} ( @@ -72,6 +73,7 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName command TEXT NOT NULL, admin_only BOOLEAN NOT NULL DEFAULT false, timeout_sec INTEGER NULL, + sort_order INTEGER NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS {$runTable} ( @@ -110,6 +112,7 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName auth_type VARCHAR(20) NOT NULL DEFAULT 'key', key_path TEXT NULL, password TEXT NULL, + image_url TEXT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS {$cmdTable} ( @@ -118,6 +121,7 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName command TEXT NOT NULL, admin_only INTEGER NOT NULL DEFAULT 0, timeout_sec INTEGER NULL, + sort_order INTEGER NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS {$runTable} ( @@ -150,7 +154,9 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName // Schema migrations for existing tables if ($driver === 'pgsql') { + $pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN IF NOT EXISTS image_url TEXT NULL"); $pdo->exec("ALTER TABLE {$cmdTable} ADD COLUMN IF NOT EXISTS timeout_sec INTEGER NULL"); + $pdo->exec("ALTER TABLE {$cmdTable} ADD COLUMN IF NOT EXISTS sort_order INTEGER NULL"); $pdo->exec("ALTER TABLE {$runTable} ADD COLUMN IF NOT EXISTS error TEXT NULL"); $pdo->exec("ALTER TABLE {$runTable} ADD COLUMN IF NOT EXISTS exit_code INTEGER NULL"); $pdo->exec("ALTER TABLE {$runTable} ADD COLUMN IF NOT EXISTS timeout_sec INTEGER NULL"); @@ -158,6 +164,15 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName $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(' . $hostTable . ')'); + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $col) { + $columns[$col['name']] = true; + } + if (empty($columns['image_url'])) { + $pdo->exec("ALTER TABLE {$hostTable} ADD COLUMN image_url TEXT NULL"); + } + $columns = []; $stmt = $pdo->query('PRAGMA table_info(' . $cmdTable . ')'); foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $col) { @@ -166,6 +181,9 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName if (empty($columns['timeout_sec'])) { $pdo->exec("ALTER TABLE {$cmdTable} ADD COLUMN timeout_sec INTEGER NULL"); } + if (empty($columns['sort_order'])) { + $pdo->exec("ALTER TABLE {$cmdTable} ADD COLUMN sort_order INTEGER NULL"); + } $columns = []; $stmt = $pdo->query('PRAGMA table_info(' . $runTable . ')'); diff --git a/modules/pi_control/module.json b/modules/pi_control/module.json index 419ddd6..631dab8 100644 --- a/modules/pi_control/module.json +++ b/modules/pi_control/module.json @@ -4,10 +4,14 @@ "description": "Verwaltung und Steuerung von Raspberry Pis (SSH/Commands/Presets).", "menu": [ { "label": "Übersicht", "href": "/module/pi_control" }, - { "label": "Hosts", "href": "/module/pi_control/hosts" }, - { "label": "Befehle", "href": "/module/pi_control/commands" }, { "label": "Konsole", "href": "/module/pi_control/console" }, - { "label": "Setup", "href": "/modules/setup/pi_control" } + { + "label": "Settings", + "children": [ + { "label": "Hosts", "href": "/module/pi_control/hosts" }, + { "label": "Befehle", "href": "/module/pi_control/commands" } + ] + } ], "sidebar": { "enabled": true, @@ -15,10 +19,9 @@ "default": "collapsed", "items": [ { "label": "Übersicht", "href": "/module/pi_control" }, - { "label": "Hosts", "href": "/module/pi_control/hosts" }, - { "label": "Befehle", "href": "/module/pi_control/commands" }, { "label": "Konsole", "href": "/module/pi_control/console" }, - { "label": "Setup", "href": "/modules/setup/pi_control" } + { "label": "Hosts", "href": "/module/pi_control/hosts" }, + { "label": "Befehle", "href": "/module/pi_control/commands" } ] }, "setup": { diff --git a/modules/pi_control/pages/asset.php b/modules/pi_control/pages/asset.php index 34f8c68..4461c27 100644 --- a/modules/pi_control/pages/asset.php +++ b/modules/pi_control/pages/asset.php @@ -4,6 +4,8 @@ $base = realpath(__DIR__ . '/../assets'); $map = [ 'pi_control.css' => $base . '/pi_control.css', 'console.js' => $base . '/console.js', + 'hosts.js' => $base . '/hosts.js', + 'commands.js' => $base . '/commands.js', ]; if (!isset($map[$file])) { diff --git a/modules/pi_control/pages/commands.php b/modules/pi_control/pages/commands.php index 9fadc18..7400df2 100644 --- a/modules/pi_control/pages/commands.php +++ b/modules/pi_control/pages/commands.php @@ -5,35 +5,79 @@ $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'); + $assets->addScript('/module/pi_control/asset?file=commands.js', 'footer', true); } $notice = null; $error = null; +if (isset($_GET['reorder_json'])) { + require_admin(); + $payload = json_decode(file_get_contents('php://input'), true); + $order = is_array($payload['order'] ?? null) ? $payload['order'] : []; + if ($order) { + $stmt = $pdo->prepare('UPDATE ' . $table('commands') . ' SET sort_order = :sort_order WHERE id = :id'); + $pos = 1; + foreach ($order as $id) { + $stmt->execute([ + 'sort_order' => $pos++, + 'id' => (int)$id, + ]); + } + } + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['ok' => true]); + exit; +} + if ($_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); + $deleteId = (int)($_POST['delete_id'] ?? 0); + $editId = (int)($_POST['id'] ?? 0); $label = trim((string)($_POST['label'] ?? '')); $command = trim((string)($_POST['command'] ?? '')); $adminOnly = !empty($_POST['admin_only']) ? 1 : 0; $timeoutSec = (int)($_POST['timeout_sec'] ?? 0); - if ($label === '' || $command === '') { - $error = 'Bitte Label und Command angeben.'; + if ($deleteId > 0) { + $stmt = $pdo->prepare('DELETE FROM ' . $table('commands') . ' WHERE id = :id'); + $stmt->execute(['id' => $deleteId]); + $notice = 'Befehl gelöscht.'; } else { - $stmt = $pdo->prepare( - 'INSERT INTO ' . $table('commands') . ' (label, command, admin_only, timeout_sec) VALUES (:label, :command, :admin_only, :timeout_sec)' - ); - $stmt->execute([ - 'label' => $label, - 'command' => $command, - 'admin_only' => $adminOnly, - 'timeout_sec' => $timeoutSec > 0 ? $timeoutSec : null, - ]); - $notice = 'Befehl gespeichert.'; + if ($label === '' || $command === '') { + $error = 'Bitte Label und Command angeben.'; + } else { + if ($editId > 0) { + $stmt = $pdo->prepare( + 'UPDATE ' . $table('commands') . ' SET label = :label, command = :command, admin_only = :admin_only, timeout_sec = :timeout_sec WHERE id = :id' + ); + $stmt->execute([ + 'id' => $editId, + 'label' => $label, + 'command' => $command, + 'admin_only' => $adminOnly, + 'timeout_sec' => $timeoutSec > 0 ? $timeoutSec : null, + ]); + $notice = 'Befehl aktualisiert.'; + } else { + $nextSort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order), 0) + 1 FROM ' . $table('commands'))->fetchColumn(); + $stmt = $pdo->prepare( + 'INSERT INTO ' . $table('commands') . ' (label, command, admin_only, timeout_sec, sort_order) VALUES (:label, :command, :admin_only, :timeout_sec, :sort_order)' + ); + $stmt->execute([ + 'label' => $label, + 'command' => $command, + 'admin_only' => $adminOnly, + 'timeout_sec' => $timeoutSec > 0 ? $timeoutSec : null, + 'sort_order' => $nextSort, + ]); + $notice = 'Befehl gespeichert.'; + } + } } } -$commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY id DESC')->fetchAll(PDO::FETCH_ASSOC); +$commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY COALESCE(sort_order, id) ASC, id ASC')->fetchAll(PDO::FETCH_ASSOC); ?>
Pi Control
@@ -53,7 +97,8 @@ $commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY id DE
Neuer Befehl -
+ + @@ -61,39 +106,56 @@ $commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY id DE Nur Admin - +
+ + +
Vorhandene Befehle -
- - - - - - - - - - - - - - - - - - - - - - -
LabelCommandTimeoutAdmin
Keine Befehle vorhanden.
+ +
Keine Befehle vorhanden.
+ +
    + +
  • +
    ⋮⋮
    +
    +
    + + + Admin + +
    +
    -
-
+
+
+ Timeout: +
+
+
+ +
+ +
+ + +
+
+
+ + + +

Reihenfolge per Drag & Drop ändern.

+
diff --git a/modules/pi_control/pages/hosts.php b/modules/pi_control/pages/hosts.php index 03309f3..e034c70 100644 --- a/modules/pi_control/pages/hosts.php +++ b/modules/pi_control/pages/hosts.php @@ -5,6 +5,7 @@ $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'); + $assets->addScript('/module/pi_control/asset?file=hosts.js', 'footer', true); } $notice = null; @@ -12,6 +13,8 @@ $error = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); + $deleteId = (int)($_POST['delete_id'] ?? 0); + $editId = (int)($_POST['id'] ?? 0); $name = trim((string)($_POST['name'] ?? '')); $host = trim((string)($_POST['host'] ?? '')); $port = (int)($_POST['port'] ?? 22); @@ -19,27 +22,137 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $authType = trim((string)($_POST['auth_type'] ?? 'key')); $keyPath = trim((string)($_POST['key_path'] ?? '')); $password = trim((string)($_POST['password'] ?? '')); + $imageUrl = trim((string)($_POST['image_url'] ?? '')); - if ($name === '' || $host === '' || $username === '') { - $error = 'Bitte Name, Host und Benutzer angeben.'; + if ($deleteId > 0) { + $stmt = $pdo->prepare('DELETE FROM ' . $table('hosts') . ' WHERE id = :id'); + $stmt->execute(['id' => $deleteId]); + $notice = 'Host gelöscht.'; } else { - $stmt = $pdo->prepare( - 'INSERT INTO ' . $table('hosts') . ' (name, host, port, username, auth_type, key_path, password) VALUES (:name, :host, :port, :username, :auth_type, :key_path, :password)' - ); - $stmt->execute([ - 'name' => $name, - 'host' => $host, - 'port' => $port > 0 ? $port : 22, - 'username' => $username, - 'auth_type' => $authType !== '' ? $authType : 'key', - 'key_path' => $keyPath !== '' ? $keyPath : null, - 'password' => $password !== '' ? $password : null, - ]); - $notice = 'Host gespeichert.'; + if ($name === '' || $host === '' || $username === '') { + $error = 'Bitte Name, Host und Benutzer angeben.'; + } else { + if ($editId > 0) { + $stmt = $pdo->prepare('SELECT * FROM ' . $table('hosts') . ' WHERE id = :id LIMIT 1'); + $stmt->execute(['id' => $editId]); + $existing = $stmt->fetch(PDO::FETCH_ASSOC) ?: []; + $passwordToStore = $password !== '' ? $password : ($existing['password'] ?? null); + $stmt = $pdo->prepare( + 'UPDATE ' . $table('hosts') . ' SET name = :name, host = :host, port = :port, username = :username, auth_type = :auth_type, key_path = :key_path, password = :password, image_url = :image_url WHERE id = :id' + ); + $stmt->execute([ + 'id' => $editId, + 'name' => $name, + 'host' => $host, + 'port' => $port > 0 ? $port : 22, + 'username' => $username, + 'auth_type' => $authType !== '' ? $authType : 'key', + 'key_path' => $keyPath !== '' ? $keyPath : null, + 'password' => $passwordToStore, + 'image_url' => $imageUrl !== '' ? $imageUrl : null, + ]); + $notice = 'Host aktualisiert.'; + } else { + $stmt = $pdo->prepare( + 'INSERT INTO ' . $table('hosts') . ' (name, host, port, username, auth_type, key_path, password, image_url) VALUES (:name, :host, :port, :username, :auth_type, :key_path, :password, :image_url)' + ); + $stmt->execute([ + 'name' => $name, + 'host' => $host, + 'port' => $port > 0 ? $port : 22, + 'username' => $username, + 'auth_type' => $authType !== '' ? $authType : 'key', + 'key_path' => $keyPath !== '' ? $keyPath : null, + 'password' => $password !== '' ? $password : null, + 'image_url' => $imageUrl !== '' ? $imageUrl : null, + ]); + $notice = 'Host gespeichert.'; + } + } } } $hosts = $pdo->query('SELECT * FROM ' . $table('hosts') . ' ORDER BY id DESC')->fetchAll(PDO::FETCH_ASSOC); +$settings = modules()->settings('pi_control'); +$strictHostKey = !empty($settings['terminal_strict_hostkey']); + +function hostReachable(string $host, int $port): bool +{ + $errno = 0; + $errstr = ''; + $fp = @fsockopen($host, $port, $errno, $errstr, 1.2); + if ($fp) { + fclose($fp); + return true; + } + return false; +} + +function runSshCommand(string $cmd, int $timeoutSec): int +{ + $descriptors = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = proc_open($cmd, $descriptors, $pipes); + if (!is_resource($process)) { + return 255; + } + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + $start = time(); + while (true) { + $status = proc_get_status($process); + if (!$status['running']) { + $code = (int)$status['exitcode']; + proc_close($process); + return $code; + } + if (time() - $start > $timeoutSec) { + proc_terminate($process, 9); + proc_close($process); + return 124; + } + usleep(100000); + } +} + +function hostAuthOk(array $host, bool $strictHostKey): bool +{ + $hostAddr = (string)($host['host'] ?? ''); + $user = (string)($host['username'] ?? ''); + $port = (int)($host['port'] ?? 22); + $authType = (string)($host['auth_type'] ?? 'key'); + $keyPath = (string)($host['key_path'] ?? ''); + $password = (string)($host['password'] ?? ''); + if ($hostAddr === '' || $user === '') { + return false; + } + + $opts = $strictHostKey + ? '-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts' + : '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'; + $opts .= ' -o ConnectTimeout=2 -o NumberOfPasswordPrompts=1'; + + $target = escapeshellarg($user . '@' . $hostAddr); + $cmd = 'ssh ' . $opts . ' -p ' . (int)$port . ' '; + if ($authType === 'key' && $keyPath !== '') { + $cmd .= '-i ' . escapeshellarg($keyPath) . ' -o BatchMode=yes '; + } elseif ($authType === 'key') { + $cmd .= '-o BatchMode=yes '; + } + $cmd .= $target . ' -- true'; + + if ($authType === 'pass') { + if ($password === '') { + return false; + } + $cmd = 'sshpass -p ' . escapeshellarg($password) . ' ' . $cmd; + } + + $exitCode = runSshCommand($cmd, 3); + return $exitCode === 0; +} ?>
Pi Control
@@ -59,7 +172,8 @@ $hosts = $pdo->query('SELECT * FROM ' . $table('hosts') . ' ORDER BY id DESC')->
Neuer Host -
+ + - + +
+ + +
Registrierte Hosts -
- - - - - - - - - - - - - - - - - - - - - - - - -
NameHostUserPortAuth
Keine Hosts vorhanden.
-
+ +
Keine Hosts vorhanden.
+ +
+ + +
+
> +
+
+
+
+
+ + +
+
+ +
+ +
+ + +
+
+
+
+
:
+
·
+
+
+ +
+
diff --git a/partials/structure/layout_start.php b/partials/structure/layout_start.php index bce449e..7515335 100755 --- a/partials/structure/layout_start.php +++ b/partials/structure/layout_start.php @@ -71,9 +71,37 @@ $sidebarItems = $moduleSidebar['items'] ?? [];
- - - + + + + + + + +
@@ -90,7 +118,13 @@ $sidebarItems = $moduleSidebar['items'] ?? []; diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 53a9dec..d54a190 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -136,7 +136,8 @@ body { display: flex; gap: 10px; flex-wrap: wrap; - background-color: #ffffff !important; + background-color: var(--panel) !important; + background: var(--panel) !important; background-image: none !important; opacity: 1 !important; backdrop-filter: none !important; @@ -147,6 +148,35 @@ body { } .module-subnav.card { background-color: var(--panel); background-image: none; } +.nav-dropdown { + position: relative; + display: inline-flex; +} +.nav-link-button { + border: 1px solid transparent; + background: transparent; + cursor: pointer; +} +.nav-dropdown-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + min-width: 180px; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 12px; + box-shadow: var(--shadow); + padding: 6px; + display: none; + z-index: 10; +} +.nav-dropdown:hover .nav-dropdown-menu, +.nav-dropdown:focus-within .nav-dropdown-menu { + display: flex; + flex-direction: column; + gap: 4px; +} + .layout-body { display: grid; grid-template-columns: 1fr;