diff --git a/modules/pi_control/assets/hosts.js b/modules/pi_control/assets/hosts.js index abddbe2..f520abd 100644 --- a/modules/pi_control/assets/hosts.js +++ b/modules/pi_control/assets/hosts.js @@ -12,14 +12,12 @@ 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 modal = document.querySelector('[data-host-modal]'); const modalTitle = document.querySelector('[data-host-modal-title]'); const closeBtn = document.querySelector('[data-host-close]'); const newBtn = document.querySelector('[data-host-new]'); const checkAllBtn = document.querySelector('[data-host-check-all]'); - const unsavedBar = document.querySelector('[data-host-unsaved]'); - const discardBtn = document.querySelector('[data-host-discard]'); + const cancelBtn = form.querySelector('[data-host-cancel]'); let initialSnapshot = ''; @@ -34,7 +32,9 @@ if (passInput) passInput.value = ''; if (imageInput) imageInput.value = ''; if (submitBtn) submitBtn.textContent = 'Speichern'; - if (unsavedBar) unsavedBar.style.display = 'none'; + if (modal && modal.classList.contains('is-open')) { + initialSnapshot = snapshot(); + } }; const snapshot = () => { @@ -63,12 +63,11 @@ const closeModal = (force = false) => { if (!modal) return; if (!force && isDirty()) { - if (unsavedBar) unsavedBar.style.display = 'flex'; - return; + const ok = window.confirm('Änderungen nicht gespeichert. Ohne Speichern schließen?'); + if (!ok) return; } modal.classList.remove('is-open'); modal.setAttribute('aria-hidden', 'true'); - if (unsavedBar) unsavedBar.style.display = 'none'; }; document.querySelectorAll('[data-host-edit]').forEach((btn) => { @@ -92,13 +91,6 @@ }); }); - if (cancelBtn) { - cancelBtn.addEventListener('click', (e) => { - e.preventDefault(); - resetForm(); - }); - } - if (newBtn) { newBtn.addEventListener('click', () => { resetForm(); @@ -111,19 +103,13 @@ closeBtn.addEventListener('click', () => closeModal(false)); } - if (discardBtn) { - discardBtn.addEventListener('click', () => { + if (cancelBtn) { + cancelBtn.addEventListener('click', (e) => { + e.preventDefault(); resetForm(); - closeModal(true); }); } - form.addEventListener('input', () => { - if (unsavedBar && isDirty()) { - unsavedBar.style.display = 'flex'; - } - }); - const updateStatus = (card, status) => { const dot = card.querySelector('[data-host-status]'); if (!dot) return; @@ -155,6 +141,7 @@ const upg = card.querySelector('[data-upgrade-badge]'); const time = card.querySelector('[data-update-time]'); if (upd) { + upd.classList.remove('badge-warn', 'badge-ok', 'badge-error'); if (data.updates && typeof data.updates.count === 'number') { upd.textContent = `Updates: ${data.updates.count}`; upd.classList.toggle('badge-warn', data.updates.count > 0); @@ -171,6 +158,7 @@ } } if (upg) { + upg.classList.remove('badge-warn', 'badge-ok', 'badge-error'); if (data.os && typeof data.os.available === 'boolean') { upg.textContent = data.os.available ? 'OS: Upgrade verfügbar' : 'OS: OK'; upg.classList.toggle('badge-warn', data.os.available); diff --git a/modules/pi_control/assets/pi_control.css b/modules/pi_control/assets/pi_control.css index 91121fd..8a1dd35 100644 --- a/modules/pi_control/assets/pi_control.css +++ b/modules/pi_control/assets/pi_control.css @@ -1,4 +1,10 @@ .form-card { padding: 14px; } + +.notice-card { + padding: 12px 14px; + line-height: 1.45; + word-break: break-word; +} .form-grid { display: grid; gap: 12px; } .form-field { display: grid; gap: 6px; } .form-field input, @@ -82,7 +88,7 @@ background: var(--panel); border: 1px solid var(--line); border-radius: 16px; - overflow: hidden; + overflow: visible; display: grid; grid-template-rows: 120px 1fr; box-shadow: var(--shadow); @@ -94,6 +100,8 @@ background-size: cover; background-position: center; position: relative; + border-top-left-radius: 16px; + border-top-right-radius: 16px; } .host-card-overlay { position: absolute; diff --git a/modules/pi_control/pages/commands.php b/modules/pi_control/pages/commands.php index 937eacb..2329c39 100644 --- a/modules/pi_control/pages/commands.php +++ b/modules/pi_control/pages/commands.php @@ -88,11 +88,11 @@ $commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY COALE

Verwalte vordefinierte SSH-Befehle.

-
+
-
+
diff --git a/modules/pi_control/pages/console.php b/modules/pi_control/pages/console.php index e485958..3269fb1 100644 --- a/modules/pi_control/pages/console.php +++ b/modules/pi_control/pages/console.php @@ -424,11 +424,11 @@ function sendToActiveConsole(array $host, string $command, bool $strictHostKey):

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

-
+
-
+
diff --git a/modules/pi_control/pages/hosts.php b/modules/pi_control/pages/hosts.php index 3bf9305..bccf75a 100644 --- a/modules/pi_control/pages/hosts.php +++ b/modules/pi_control/pages/hosts.php @@ -46,26 +46,36 @@ if (isset($_GET['update_json'])) { $settings = modules()->settings('pi_control'); $strictHostKey = !empty($settings['terminal_strict_hostkey']); - $updateCmd = "apt-get -s upgrade | grep '^Inst'"; - $upgradeCmd = 'current="$(. /etc/os-release && echo "$VERSION_CODENAME")"; latest="$(curl -fsSL https://deb.debian.org/debian/dists/stable/Release | awk -F\': \' \'/^Codename:/{print $2}\')"; echo "Installed: $current"; echo "Latest stable: $latest"; [ "$current" != "$latest" ] && echo "OS UPGRADE AVAILABLE" || echo "NO OS UPGRADE AVAILABLE"'; + $updateCmd = "sh -lc 'if ! command -v apt-get >/dev/null 2>&1; then echo \"__ERR__NO_APT\"; exit 2; fi; apt-get -s upgrade 2>/dev/null | grep \"^Inst \" || true'"; + $upgradeCmd = "sh -lc 'current=\"$(. /etc/os-release 2>/dev/null && echo \"$VERSION_CODENAME\")\"; if [ -z \"$current\" ]; then echo \"__ERR__NO_CODENAME\"; exit 2; fi; latest=\"$( (command -v curl >/dev/null 2>&1 && curl -fsSL https://deb.debian.org/debian/dists/stable/Release) || (command -v wget >/dev/null 2>&1 && wget -qO- https://deb.debian.org/debian/dists/stable/Release) )\"; latest=\"$(printf \"%s\" \"$latest\" | awk -F\": \" \"/^Codename:/{print $2; exit}\")\"; if [ -z \"$latest\" ]; then echo \"__ERR__NO_LATEST\"; exit 3; fi; echo \"__CURRENT__=$current\"; echo \"__LATEST__=$latest\"; if [ \"$current\" != \"$latest\" ]; then echo \"__UPGRADE__=1\"; else echo \"__UPGRADE__=0\"; fi'"; [$updExit, $updOut, $updErr] = runSshCommandCapture($host, $updateCmd, $strictHostKey, 20); - $lines = array_values(array_filter(preg_split('/\r?\n/', (string)$updOut))); - $updateCount = $updExit === 0 ? count($lines) : null; - $updatePreview = $updateCount ? implode("\n", array_slice($lines, 0, 6)) : ''; + $updOutStr = (string)$updOut; + $updErrStr = (string)$updErr; + $updateCount = null; + $updatePreview = ''; + if ($updExit === 0 && !str_contains($updOutStr, '__ERR__')) { + $lines = array_values(array_filter(preg_split('/\r?\n/', $updOutStr))); + $updateCount = count($lines); + $updatePreview = $updateCount ? implode("\n", array_slice($lines, 0, 6)) : ''; + } [$upgExit, $upgOut, $upgErr] = runSshCommandCapture($host, $upgradeCmd, $strictHostKey, 25); + $upgOutStr = (string)$upgOut; + $upgErrStr = (string)$upgErr; $upgradeAvailable = null; - if ($upgExit === 0) { - $upgradeAvailable = str_contains($upgOut, 'OS UPGRADE AVAILABLE'); + if ($upgExit === 0 && !str_contains($upgOutStr, '__ERR__')) { + if (preg_match('/^__UPGRADE__=(0|1)$/m', $upgOutStr, $m)) { + $upgradeAvailable = $m[1] === '1'; + } } $driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME); $nowExpr = $driver === 'pgsql' ? 'NOW()' : "DATETIME('now')"; $updCountVal = $updateCount; - $updErrVal = $updExit === 0 ? null : trim($updErr ?: $updOut); + $updErrVal = $updExit === 0 && !str_contains($updOutStr, '__ERR__') ? null : trim($updErrStr ?: $updOutStr); $upgAvailVal = $upgradeAvailable; - $upgErrVal = $upgExit === 0 ? null : trim($upgErr ?: $upgOut); + $upgErrVal = $upgExit === 0 && !str_contains($upgOutStr, '__ERR__') ? null : trim($upgErrStr ?: $upgOutStr); $stmt = $pdo->prepare( 'UPDATE ' . $table('hosts') . ' SET update_checked_at = ' . $nowExpr . ', update_count = :update_count, @@ -81,7 +91,7 @@ if (isset($_GET['update_json'])) { 'update_preview' => $updatePreview !== '' ? $updatePreview : null, 'update_error' => $updErrVal, 'upgrade_available' => $upgAvailVal === null ? null : ($upgAvailVal ? 1 : 0), - 'upgrade_raw' => $upgExit === 0 ? trim($upgOut) : null, + 'upgrade_raw' => $upgExit === 0 ? trim($upgOutStr) : null, 'upgrade_error' => $upgErrVal, 'id' => $id, ]); @@ -92,12 +102,12 @@ if (isset($_GET['update_json'])) { 'updates' => [ 'count' => $updateCount, 'preview' => $updatePreview, - 'error' => $updExit === 0 ? '' : trim($updErr ?: $updOut), + 'error' => $updExit === 0 && !str_contains($updOutStr, '__ERR__') ? '' : trim($updErrStr ?: $updOutStr), ], 'os' => [ 'available' => $upgradeAvailable, - 'raw' => $upgExit === 0 ? trim($upgOut) : '', - 'error' => $upgExit === 0 ? '' : trim($upgErr ?: $upgOut), + 'raw' => $upgExit === 0 ? trim($upgOutStr) : '', + 'error' => $upgExit === 0 && !str_contains($upgOutStr, '__ERR__') ? '' : trim($upgErrStr ?: $upgOutStr), ], 'checked_at' => date('c'), ]); @@ -317,11 +327,11 @@ function hostAuthOk(array $host, bool $strictHostKey): bool

Verwalte die Raspberry Pis, die du steuern möchtest.

-
+
-
+
@@ -431,13 +441,6 @@ function hostAuthOk(array $host, bool $strictHostKey): bool Bild-URL (optional) -
diff --git a/tools/pi_control/check_updates.php b/tools/pi_control/check_updates.php index 110b713..c416a54 100644 --- a/tools/pi_control/check_updates.php +++ b/tools/pi_control/check_updates.php @@ -13,8 +13,8 @@ $table = fn(string $name) => module_fn($module, 'table', $name); $settings = modules()->settings($module); $strictHostKey = !empty($settings['terminal_strict_hostkey']); -$updateCmd = "apt-get -s upgrade | grep '^Inst'"; -$upgradeCmd = 'current="$(. /etc/os-release && echo "$VERSION_CODENAME")"; latest="$(curl -fsSL https://deb.debian.org/debian/dists/stable/Release | awk -F\': \' \'/^Codename:/{print $2}\')"; echo "Installed: $current"; echo "Latest stable: $latest"; [ "$current" != "$latest" ] && echo "OS UPGRADE AVAILABLE" || echo "NO OS UPGRADE AVAILABLE"'; +$updateCmd = "sh -lc 'if ! command -v apt-get >/dev/null 2>&1; then echo \"__ERR__NO_APT\"; exit 2; fi; apt-get -s upgrade 2>/dev/null | grep \"^Inst \" || true'"; +$upgradeCmd = "sh -lc 'current=\"$(. /etc/os-release 2>/dev/null && echo \"$VERSION_CODENAME\")\"; if [ -z \"$current\" ]; then echo \"__ERR__NO_CODENAME\"; exit 2; fi; latest=\"$( (command -v curl >/dev/null 2>&1 && curl -fsSL https://deb.debian.org/debian/dists/stable/Release) || (command -v wget >/dev/null 2>&1 && wget -qO- https://deb.debian.org/debian/dists/stable/Release) )\"; latest=\"$(printf \"%s\" \"$latest\" | awk -F\": \" \"/^Codename:/{print $2; exit}\")\"; if [ -z \"$latest\" ]; then echo \"__ERR__NO_LATEST\"; exit 3; fi; echo \"__CURRENT__=$current\"; echo \"__LATEST__=$latest\"; if [ \"$current\" != \"$latest\" ]; then echo \"__UPGRADE__=1\"; else echo \"__UPGRADE__=0\"; fi'"; $driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME); $nowExpr = $driver === 'pgsql' ? 'NOW()' : "DATETIME('now')"; @@ -26,18 +26,28 @@ foreach ($hosts as $host) { if ($id <= 0) continue; [$updExit, $updOut, $updErr] = runSshCommandCapture($host, $updateCmd, $strictHostKey, 20); - $lines = array_values(array_filter(preg_split('/\r?\n/', (string)$updOut))); - $updateCount = $updExit === 0 ? count($lines) : null; - $updatePreview = $updateCount ? implode("\n", array_slice($lines, 0, 6)) : ''; - - [$upgExit, $upgOut, $upgErr] = runSshCommandCapture($host, $upgradeCmd, $strictHostKey, 25); - $upgradeAvailable = null; - if ($upgExit === 0) { - $upgradeAvailable = str_contains($upgOut, 'OS UPGRADE AVAILABLE'); + $updOutStr = (string)$updOut; + $updErrStr = (string)$updErr; + $updateCount = null; + $updatePreview = ''; + if ($updExit === 0 && !str_contains($updOutStr, '__ERR__')) { + $lines = array_values(array_filter(preg_split('/\r?\n/', $updOutStr))); + $updateCount = count($lines); + $updatePreview = $updateCount ? implode("\n", array_slice($lines, 0, 6)) : ''; } - $updErrVal = $updExit === 0 ? null : trim($updErr ?: $updOut); - $upgErrVal = $upgExit === 0 ? null : trim($upgErr ?: $upgOut); + [$upgExit, $upgOut, $upgErr] = runSshCommandCapture($host, $upgradeCmd, $strictHostKey, 25); + $upgOutStr = (string)$upgOut; + $upgErrStr = (string)$upgErr; + $upgradeAvailable = null; + if ($upgExit === 0 && !str_contains($upgOutStr, '__ERR__')) { + if (preg_match('/^__UPGRADE__=(0|1)$/m', $upgOutStr, $m)) { + $upgradeAvailable = $m[1] === '1'; + } + } + + $updErrVal = $updExit === 0 && !str_contains($updOutStr, '__ERR__') ? null : trim($updErrStr ?: $updOutStr); + $upgErrVal = $upgExit === 0 && !str_contains($upgOutStr, '__ERR__') ? null : trim($upgErrStr ?: $upgOutStr); $stmt = $pdo->prepare( 'UPDATE ' . $table('hosts') . ' SET update_checked_at = ' . $nowExpr . ', @@ -54,7 +64,7 @@ foreach ($hosts as $host) { 'update_preview' => $updatePreview !== '' ? $updatePreview : null, 'update_error' => $updErrVal, 'upgrade_available' => $upgradeAvailable === null ? null : ($upgradeAvailable ? 1 : 0), - 'upgrade_raw' => $upgExit === 0 ? trim($upgOut) : null, + 'upgrade_raw' => $upgExit === 0 ? trim($upgOutStr) : null, 'upgrade_error' => $upgErrVal, 'id' => $id, ]);