From 5687fbb21bb42658c7512dab7b61674d95e2ce02 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Mon, 9 Mar 2026 02:00:04 +0100 Subject: [PATCH] pi hole update --- modules/pihole/assets/pihole.css | 62 ++++++ modules/pihole/assets/pihole_instances.js | 67 +++++++ modules/pihole/module.json | 2 + modules/pihole/pages/asset.php | 1 + modules/pihole/pages/instances.php | 222 ++++++++++++++++++++++ 5 files changed, 354 insertions(+) create mode 100644 modules/pihole/assets/pihole_instances.js create mode 100644 modules/pihole/pages/instances.php diff --git a/modules/pihole/assets/pihole.css b/modules/pihole/assets/pihole.css index 2de22a5..c38eaa4 100644 --- a/modules/pihole/assets/pihole.css +++ b/modules/pihole/assets/pihole.css @@ -157,6 +157,68 @@ gap: 6px; } +.pihole-instance-card { + padding: 16px; + background: var(--panel-2); + display: grid; + gap: 12px; +} + +.pihole-card-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; +} + +.form-field { + display: grid; + gap: 6px; +} + +.icon-button { + border: 1px solid var(--line); + background: var(--panel); + border-radius: 10px; + width: 36px; + height: 36px; + cursor: pointer; + font-size: 1.1rem; +} + +.modal { + position: fixed; + inset: 0; + background: rgba(10, 14, 24, 0.55); + display: none; + align-items: center; + justify-content: center; + padding: 24px; + z-index: 40; +} +.modal.is-open { display: flex; } +.modal-card { + width: min(880px, 96vw); + max-height: 90vh; + overflow: auto; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 16px; + box-shadow: var(--shadow); + padding: 16px; +} +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + @media (max-width: 680px) { .pihole-actions { flex-direction: column; diff --git a/modules/pihole/assets/pihole_instances.js b/modules/pihole/assets/pihole_instances.js new file mode 100644 index 0000000..ec282f7 --- /dev/null +++ b/modules/pihole/assets/pihole_instances.js @@ -0,0 +1,67 @@ +(() => { + const modal = document.querySelector('[data-instance-modal]'); + const form = document.querySelector('[data-instance-form]'); + if (!modal || !form) return; + + const title = document.querySelector('[data-instance-modal-title]'); + const closeBtn = document.querySelector('[data-instance-close]'); + const newBtn = document.querySelector('[data-instance-new]'); + const cancelBtn = document.querySelector('[data-instance-cancel]'); + + const currentIdInput = form.querySelector('input[name="current_id"]'); + const idInput = form.querySelector('input[name="instance_id"]'); + const nameInput = form.querySelector('input[name="name"]'); + const urlInput = form.querySelector('input[name="url"]'); + const tokenInput = form.querySelector('input[name="token"]'); + const primaryInput = form.querySelector('input[name="is_primary"]'); + + const resetForm = () => { + if (currentIdInput) currentIdInput.value = ''; + if (idInput) idInput.value = ''; + if (nameInput) nameInput.value = ''; + if (urlInput) urlInput.value = ''; + if (tokenInput) tokenInput.value = ''; + if (primaryInput) primaryInput.checked = false; + }; + + const openModal = () => { + modal.classList.add('is-open'); + modal.setAttribute('aria-hidden', 'false'); + }; + + const closeModal = () => { + modal.classList.remove('is-open'); + modal.setAttribute('aria-hidden', 'true'); + }; + + document.querySelectorAll('[data-instance-edit]').forEach((btn) => { + btn.addEventListener('click', () => { + const card = btn.closest('[data-instance-id]'); + if (!card) return; + if (currentIdInput) currentIdInput.value = card.dataset.instanceId || ''; + if (idInput) idInput.value = card.dataset.instanceId || ''; + if (nameInput) nameInput.value = card.dataset.name || ''; + if (urlInput) urlInput.value = card.dataset.url || ''; + if (tokenInput) tokenInput.value = ''; + if (primaryInput) primaryInput.checked = card.dataset.primary === '1'; + if (title) title.textContent = 'Instanz bearbeiten'; + openModal(); + }); + }); + + if (newBtn) { + newBtn.addEventListener('click', () => { + resetForm(); + if (title) title.textContent = 'Neue Instanz'; + openModal(); + }); + } + + if (closeBtn) { + closeBtn.addEventListener('click', () => closeModal()); + } + + if (cancelBtn) { + cancelBtn.addEventListener('click', () => resetForm()); + } +})(); diff --git a/modules/pihole/module.json b/modules/pihole/module.json index c8d5ef2..1658d12 100644 --- a/modules/pihole/module.json +++ b/modules/pihole/module.json @@ -4,6 +4,7 @@ "description": "Pi-hole Monitoring, Listen und Steuerung fuer zwei Instanzen.", "menu": [ { "label": "Dashboard", "href": "/module/pihole" }, + { "label": "Instanzen", "href": "/module/pihole/instances" }, { "label": "Listen", "href": "/module/pihole/lists" }, { "label": "Zugriffe", "href": "/module/pihole/queries" }, { "label": "Setup", "href": "/modules/setup/pihole" } @@ -14,6 +15,7 @@ "default": "collapsed", "items": [ { "label": "Dashboard", "href": "/module/pihole" }, + { "label": "Instanzen", "href": "/module/pihole/instances" }, { "label": "Listen", "href": "/module/pihole/lists" }, { "label": "Zugriffe", "href": "/module/pihole/queries" }, { "label": "Setup", "href": "/modules/setup/pihole" } diff --git a/modules/pihole/pages/asset.php b/modules/pihole/pages/asset.php index cff4b24..5fbd20e 100644 --- a/modules/pihole/pages/asset.php +++ b/modules/pihole/pages/asset.php @@ -4,6 +4,7 @@ $base = realpath(__DIR__ . '/../assets'); $map = [ 'pihole.css' => $base . '/pihole.css', 'pihole.js' => $base . '/pihole.js', + 'pihole_instances.js' => $base . '/pihole_instances.js', ]; if (!isset($map[$file])) { diff --git a/modules/pihole/pages/instances.php b/modules/pihole/pages/instances.php new file mode 100644 index 0000000..693d795 --- /dev/null +++ b/modules/pihole/pages/instances.php @@ -0,0 +1,222 @@ +assets(); +$assets->addStyle('/module/pihole/asset?file=pihole.css'); +$assets->addScript('/module/pihole/asset?file=pihole_instances.js', 'footer', true); + +require_admin(); + +$settings = modules()->settings($moduleName); +$notice = null; +$error = null; + +$loadInstances = function (array $settings): array { + $instances = []; + $rawJson = trim((string)($settings['instances_json'] ?? '')); + if ($rawJson !== '') { + $decoded = json_decode($rawJson, true); + if (is_array($decoded)) { + foreach ($decoded as $row) { + if (!is_array($row)) { + continue; + } + $id = trim((string)($row['id'] ?? '')); + $url = trim((string)($row['url'] ?? '')); + if ($id === '' || $url === '') { + continue; + } + $instances[$id] = [ + 'id' => $id, + 'name' => trim((string)($row['name'] ?? '')) ?: $id, + 'url' => $url, + 'token' => trim((string)($row['token'] ?? '')), + 'is_primary' => !empty($row['is_primary']), + ]; + } + } + } + + if (!$instances) { + foreach (['primary', 'secondary'] as $key) { + $url = trim((string)($settings[$key . '_url'] ?? '')); + if ($url === '') { + continue; + } + $instances[$key] = [ + 'id' => $key, + 'name' => trim((string)($settings[$key . '_name'] ?? '')) ?: ($key === 'primary' ? 'Primaer' : 'Sekundaer'), + 'url' => $url, + 'token' => trim((string)($settings[$key . '_token'] ?? '')), + 'is_primary' => $key === 'primary', + ]; + } + } + + return $instances; +}; + +$instances = $loadInstances($settings); + +$sanitizeId = function (string $id): string { + $id = preg_replace('/[^a-zA-Z0-9_-]/', '', $id); + return trim((string)$id); +}; + +$saveInstances = function (array $settings, array $instances): void { + $payload = $settings; + $payload['instances_json'] = json_encode(array_values($instances), JSON_UNESCAPED_UNICODE); + modules()->saveSettings('pihole', $payload); +}; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $deleteId = trim((string)($_POST['delete_id'] ?? '')); + $currentId = trim((string)($_POST['current_id'] ?? '')); + $instanceId = trim((string)($_POST['instance_id'] ?? '')); + $name = trim((string)($_POST['name'] ?? '')); + $url = trim((string)($_POST['url'] ?? '')); + $token = trim((string)($_POST['token'] ?? '')); + $isPrimary = isset($_POST['is_primary']); + + if ($deleteId !== '') { + if (isset($instances[$deleteId])) { + unset($instances[$deleteId]); + $notice = 'Instanz geloescht.'; + } + } else { + $instanceId = $sanitizeId($instanceId); + if ($instanceId === '' || $url === '') { + $error = 'Bitte ID und URL angeben.'; + } else { + $existingToken = ''; + if ($currentId !== '' && isset($instances[$currentId])) { + $existingToken = (string)($instances[$currentId]['token'] ?? ''); + } + $tokenToStore = $token !== '' ? $token : $existingToken; + if ($currentId !== '' && $currentId !== $instanceId) { + unset($instances[$currentId]); + } + $instances[$instanceId] = [ + 'id' => $instanceId, + 'name' => $name !== '' ? $name : $instanceId, + 'url' => $url, + 'token' => $tokenToStore, + 'is_primary' => $isPrimary, + ]; + + if ($isPrimary) { + foreach ($instances as $id => &$row) { + $row['is_primary'] = ($id === $instanceId); + } + unset($row); + $settings['primary_id'] = $instanceId; + } + + $notice = $currentId !== '' ? 'Instanz aktualisiert.' : 'Instanz gespeichert.'; + } + } + + if (!$error) { + $saveInstances($settings, $instances); + $settings = modules()->settings($moduleName); + $instances = $loadInstances($settings); + } +} + +$primaryId = trim((string)($settings['primary_id'] ?? '')); +if ($primaryId === '') { + foreach ($instances as $id => $row) { + if (!empty($row['is_primary'])) { + $primaryId = $id; + break; + } + } +} +?> +
+
Pi-hole
+
+

Instanzen

+
+ +
+
+

Pi-hole Instanzen hinzufuegen, bearbeiten und loeschen.

+ + +
+ +
+ +
+ +
+ + +
+ +
Keine Instanzen vorhanden.
+ + +
+
+
+ +
ID:
+
URL:
+
+ + Primaer + +
+
+ +
+ + +
+
+
+ + +
+
+ +