From 8de05d5552e50a511d4a7a229f73961799e42d80 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Mon, 9 Mar 2026 02:24:29 +0100 Subject: [PATCH] pi hole setup --- modules/pihole/assets/pihole.css | 17 ++++++ modules/pihole/assets/pihole.js | 15 +++++ modules/pihole/assets/pihole_instances.js | 38 ++++++++++++ modules/pihole/pages/api.php | 72 ++++++++++++++++++++--- modules/pihole/pages/index.php | 1 + modules/pihole/pages/instances.php | 2 + 6 files changed, 138 insertions(+), 7 deletions(-) diff --git a/modules/pihole/assets/pihole.css b/modules/pihole/assets/pihole.css index c38eaa4..03b098e 100644 --- a/modules/pihole/assets/pihole.css +++ b/modules/pihole/assets/pihole.css @@ -128,6 +128,12 @@ color: var(--muted); } +.pihole-error { + margin-top: 8px; + font-size: 0.9rem; + color: #a83a28; +} + .pihole-blocked { display: grid; gap: 8px; @@ -170,6 +176,17 @@ flex-wrap: wrap; } +.pihole-test-result { + margin-top: 6px; + font-size: 0.9rem; + color: var(--muted); +} +.pihole-test-result.is-ok { color: #0a6b63; } +.pihole-test-result.is-auth { color: #a83a28; } +.pihole-test-result.is-unreachable { color: #a83a28; } +.pihole-test-result.is-invalid { color: #8a5a00; } +.pihole-test-result.is-error { color: #a83a28; } + .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); diff --git a/modules/pihole/assets/pihole.js b/modules/pihole/assets/pihole.js index 11d8603..804483e 100644 --- a/modules/pihole/assets/pihole.js +++ b/modules/pihole/assets/pihole.js @@ -131,6 +131,21 @@ updateEl.textContent = 'Keine Updates erkannt'; } + const errorEl = root.querySelector('[data-instance-errors]'); + if (errorEl) { + if (Array.isArray(entry.errors) && entry.errors.length) { + const lines = entry.errors.map((err) => { + const code = err.http_code ? `HTTP ${err.http_code}` : 'HTTP ?'; + const scope = err.scope || 'request'; + const msg = err.error || 'error'; + return `${scope}: ${msg} (${code})`; + }); + errorEl.textContent = `API Fehler: ${lines.join(' | ')}`; + } else { + errorEl.textContent = ''; + } + } + holder.appendChild(node); }); }; diff --git a/modules/pihole/assets/pihole_instances.js b/modules/pihole/assets/pihole_instances.js index ec282f7..0f07989 100644 --- a/modules/pihole/assets/pihole_instances.js +++ b/modules/pihole/assets/pihole_instances.js @@ -49,6 +49,44 @@ }); }); + const apiCall = async (action, payload = {}) => { + const res = await fetch(`/module/pihole/api?action=${encodeURIComponent(action)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); + return data; + }; + + document.querySelectorAll('[data-instance-test]').forEach((btn) => { + btn.addEventListener('click', async () => { + const card = btn.closest('[data-instance-id]'); + if (!card) return; + const instanceId = card.dataset.instanceId || ''; + const resultEl = card.querySelector('[data-instance-result]'); + if (resultEl) { + resultEl.classList.remove('is-ok', 'is-auth', 'is-unreachable', 'is-invalid', 'is-error'); + resultEl.textContent = 'Teste Verbindung...'; + } + try { + const res = await apiCall('test', { instance: instanceId }); + if (resultEl) { + const statusClass = res.status ? `is-${res.status}` : 'is-ok'; + resultEl.classList.add(statusClass); + resultEl.textContent = res.message || 'Verbindung OK.'; + } + } catch (err) { + if (resultEl) { + const msg = err.message || 'Fehler'; + resultEl.classList.add('is-error'); + resultEl.textContent = `Test fehlgeschlagen: ${msg}`; + } + } + }); + }); + if (newBtn) { newBtn.addEventListener('click', () => { resetForm(); diff --git a/modules/pihole/pages/api.php b/modules/pihole/pages/api.php index addb72d..d54f8e9 100644 --- a/modules/pihole/pages/api.php +++ b/modules/pihole/pages/api.php @@ -220,42 +220,51 @@ if ($action === 'dashboard') { $instancePayloads = []; $statuses = []; + $makeError = function (string $scope, array $result): array { + return [ + 'scope' => $scope, + 'error' => $result['error'] ?? 'error', + 'http_code' => $result['http_code'] ?? 0, + 'url' => $result['url'] ?? '', + ]; + }; + foreach ($instances as $id => $instance) { $errors = []; $summary = $apiRequest($instance, ['summaryRaw' => 1]); if (!$summary['ok']) { - $errors[] = ['scope' => 'summary', 'error' => $summary['error'] ?? 'error']; + $errors[] = $makeError('summary', $summary); } $topItems = $apiRequest($instance, ['topItems' => 50]); if (!$topItems['ok']) { - $errors[] = ['scope' => 'topItems', 'error' => $topItems['error'] ?? 'error']; + $errors[] = $makeError('topItems', $topItems); } $queryTypes = $apiRequest($instance, ['getQueryTypes' => 1]); if (!$queryTypes['ok']) { - $errors[] = ['scope' => 'queryTypes', 'error' => $queryTypes['error'] ?? 'error']; + $errors[] = $makeError('queryTypes', $queryTypes); } $querySources = $apiRequest($instance, ['getQuerySources' => 1]); if (!$querySources['ok']) { - $errors[] = ['scope' => 'querySources', 'error' => $querySources['error'] ?? 'error']; + $errors[] = $makeError('querySources', $querySources); } $forwardDest = $apiRequest($instance, ['getForwardDestinations' => 1]); if (!$forwardDest['ok']) { - $errors[] = ['scope' => 'forwardDestinations', 'error' => $forwardDest['error'] ?? 'error']; + $errors[] = $makeError('forwardDestinations', $forwardDest); } $recentBlocked = $apiRequest($instance, ['recentBlocked' => 30]); if (!$recentBlocked['ok']) { - $errors[] = ['scope' => 'recentBlocked', 'error' => $recentBlocked['error'] ?? 'error']; + $errors[] = $makeError('recentBlocked', $recentBlocked); } $versions = $apiRequest($instance, ['versions' => 1]); if (!$versions['ok']) { - $errors[] = ['scope' => 'versions', 'error' => $versions['error'] ?? 'error']; + $errors[] = $makeError('versions', $versions); } $summaryData = $summary['ok'] ? $summary['data'] : null; @@ -364,6 +373,55 @@ if ($action === 'dashboard') { ]); } +if ($action === 'test') { + require_admin(); + $target = (string)($payload['instance'] ?? ''); + if ($target === '') { + $respond(['ok' => false, 'error' => 'missing_instance'], 400); + } + if (!isset($instances[$target])) { + $respond(['ok' => false, 'error' => 'invalid_instance'], 400); + } + + $instance = $instances[$target]; + $result = $apiRequest($instance, ['summaryRaw' => 1]); + if ($result['ok']) { + $respond([ + 'ok' => true, + 'status' => 'ok', + 'message' => 'Verbindung OK. API antwortet.', + ]); + } + + $httpCode = (int)($result['http_code'] ?? 0); + $error = (string)($result['error'] ?? 'error'); + $status = 'error'; + $message = 'Unbekannter Fehler.'; + + if ($httpCode === 0) { + $status = 'unreachable'; + $message = 'Host nicht erreichbar oder kein HTTP-Response.'; + } elseif ($httpCode === 401 || $httpCode === 403) { + $status = 'auth'; + $message = 'API Token/Passwort falsch oder nicht berechtigt.'; + } elseif ($error === 'invalid_json') { + $status = 'invalid'; + $message = 'API antwortet nicht mit JSON. URL oder API-Pfad pruefen.'; + } else { + $status = 'error'; + $message = 'API Fehler: ' . $error . ' (HTTP ' . $httpCode . ')'; + } + + $respond([ + 'ok' => false, + 'status' => $status, + 'message' => $message, + 'http_code' => $httpCode, + 'error' => $error, + 'url' => (string)($result['url'] ?? ''), + ]); +} + if ($action === 'disable') { require_admin(); $minutes = (int)($payload['minutes'] ?? 0); diff --git a/modules/pihole/pages/index.php b/modules/pihole/pages/index.php index fa82af5..4e9a5a3 100644 --- a/modules/pihole/pages/index.php +++ b/modules/pihole/pages/index.php @@ -131,5 +131,6 @@ $hasConfig = !empty($instances);
+
diff --git a/modules/pihole/pages/instances.php b/modules/pihole/pages/instances.php index 693d795..f9df940 100644 --- a/modules/pihole/pages/instances.php +++ b/modules/pihole/pages/instances.php @@ -174,11 +174,13 @@ if ($primaryId === '') {
+
+