pi hole setup

This commit is contained in:
2026-03-09 02:24:29 +01:00
parent c144d9bcd2
commit 8de05d5552
6 changed files with 138 additions and 7 deletions

View File

@@ -128,6 +128,12 @@
color: var(--muted); color: var(--muted);
} }
.pihole-error {
margin-top: 8px;
font-size: 0.9rem;
color: #a83a28;
}
.pihole-blocked { .pihole-blocked {
display: grid; display: grid;
gap: 8px; gap: 8px;
@@ -170,6 +176,17 @@
flex-wrap: wrap; 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 { .form-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));

View File

@@ -131,6 +131,21 @@
updateEl.textContent = 'Keine Updates erkannt'; 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); holder.appendChild(node);
}); });
}; };

View File

@@ -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) { if (newBtn) {
newBtn.addEventListener('click', () => { newBtn.addEventListener('click', () => {
resetForm(); resetForm();

View File

@@ -220,42 +220,51 @@ if ($action === 'dashboard') {
$instancePayloads = []; $instancePayloads = [];
$statuses = []; $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) { foreach ($instances as $id => $instance) {
$errors = []; $errors = [];
$summary = $apiRequest($instance, ['summaryRaw' => 1]); $summary = $apiRequest($instance, ['summaryRaw' => 1]);
if (!$summary['ok']) { if (!$summary['ok']) {
$errors[] = ['scope' => 'summary', 'error' => $summary['error'] ?? 'error']; $errors[] = $makeError('summary', $summary);
} }
$topItems = $apiRequest($instance, ['topItems' => 50]); $topItems = $apiRequest($instance, ['topItems' => 50]);
if (!$topItems['ok']) { if (!$topItems['ok']) {
$errors[] = ['scope' => 'topItems', 'error' => $topItems['error'] ?? 'error']; $errors[] = $makeError('topItems', $topItems);
} }
$queryTypes = $apiRequest($instance, ['getQueryTypes' => 1]); $queryTypes = $apiRequest($instance, ['getQueryTypes' => 1]);
if (!$queryTypes['ok']) { if (!$queryTypes['ok']) {
$errors[] = ['scope' => 'queryTypes', 'error' => $queryTypes['error'] ?? 'error']; $errors[] = $makeError('queryTypes', $queryTypes);
} }
$querySources = $apiRequest($instance, ['getQuerySources' => 1]); $querySources = $apiRequest($instance, ['getQuerySources' => 1]);
if (!$querySources['ok']) { if (!$querySources['ok']) {
$errors[] = ['scope' => 'querySources', 'error' => $querySources['error'] ?? 'error']; $errors[] = $makeError('querySources', $querySources);
} }
$forwardDest = $apiRequest($instance, ['getForwardDestinations' => 1]); $forwardDest = $apiRequest($instance, ['getForwardDestinations' => 1]);
if (!$forwardDest['ok']) { if (!$forwardDest['ok']) {
$errors[] = ['scope' => 'forwardDestinations', 'error' => $forwardDest['error'] ?? 'error']; $errors[] = $makeError('forwardDestinations', $forwardDest);
} }
$recentBlocked = $apiRequest($instance, ['recentBlocked' => 30]); $recentBlocked = $apiRequest($instance, ['recentBlocked' => 30]);
if (!$recentBlocked['ok']) { if (!$recentBlocked['ok']) {
$errors[] = ['scope' => 'recentBlocked', 'error' => $recentBlocked['error'] ?? 'error']; $errors[] = $makeError('recentBlocked', $recentBlocked);
} }
$versions = $apiRequest($instance, ['versions' => 1]); $versions = $apiRequest($instance, ['versions' => 1]);
if (!$versions['ok']) { if (!$versions['ok']) {
$errors[] = ['scope' => 'versions', 'error' => $versions['error'] ?? 'error']; $errors[] = $makeError('versions', $versions);
} }
$summaryData = $summary['ok'] ? $summary['data'] : null; $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') { if ($action === 'disable') {
require_admin(); require_admin();
$minutes = (int)($payload['minutes'] ?? 0); $minutes = (int)($payload['minutes'] ?? 0);

View File

@@ -131,5 +131,6 @@ $hasConfig = !empty($instances);
<button class="nav-link" data-action="update" data-instance="">Pi-hole Update</button> <button class="nav-link" data-action="update" data-instance="">Pi-hole Update</button>
</div> </div>
<div class="pihole-update" data-instance-update></div> <div class="pihole-update" data-instance-update></div>
<div class="pihole-error" data-instance-errors></div>
</div> </div>
</template> </template>

View File

@@ -174,11 +174,13 @@ if ($primaryId === '') {
</div> </div>
<div class="pihole-card-actions"> <div class="pihole-card-actions">
<button class="nav-link" type="button" data-instance-edit>Bearbeiten</button> <button class="nav-link" type="button" data-instance-edit>Bearbeiten</button>
<button class="nav-link" type="button" data-instance-test>Test Verbindung</button>
<form method="post" onsubmit="return confirm('Instanz wirklich loeschen?')"> <form method="post" onsubmit="return confirm('Instanz wirklich loeschen?')">
<input type="hidden" name="delete_id" value="<?= e((string)($instance['id'] ?? '')) ?>"> <input type="hidden" name="delete_id" value="<?= e((string)($instance['id'] ?? '')) ?>">
<button class="nav-link" type="submit">Loeschen</button> <button class="nav-link" type="submit">Loeschen</button>
</form> </form>
</div> </div>
<div class="pihole-test-result" data-instance-result></div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>