diff --git a/modules/pihole/assets/pihole.js b/modules/pihole/assets/pihole.js index 7c3b3ae..48616fe 100644 --- a/modules/pihole/assets/pihole.js +++ b/modules/pihole/assets/pihole.js @@ -205,6 +205,79 @@ }); }; + const renderDashboardData = (data) => { + const summary = data.aggregate?.summary || {}; + setText(document.querySelector('[data-summary-dns]'), fmt.format(Number(summary.dns_queries_today || 0))); + setText(document.querySelector('[data-summary-blocked]'), fmt.format(Number(summary.ads_blocked_today || 0))); + setText(document.querySelector('[data-summary-percent]'), `${Number(summary.ads_percentage_today || 0).toFixed(2)}%`); + setText(document.querySelector('[data-summary-clients]'), fmt.format(Number(summary.unique_clients || 0))); + setStatusBadge(document.querySelector('[data-summary-status]'), summary.status || 'unknown'); + setText(document.querySelector('[data-summary-last-refresh]'), `Letztes Update: ${fmtDate(data.ts)}`); + + renderInstances(data.instances || {}); + renderList(document.querySelector('[data-query-types]'), data.aggregate?.query_types, 'Keine Daten'); + renderList(document.querySelector('[data-forward-destinations]'), data.aggregate?.forward_destinations, 'Keine Daten'); + renderList(document.querySelector('[data-top-ads]'), data.aggregate?.top_ads, 'Keine Daten'); + renderList(document.querySelector('[data-top-queries]'), data.aggregate?.top_queries, 'Keine Daten'); + renderList(document.querySelector('[data-top-clients]'), data.aggregate?.query_sources, 'Keine Daten'); + renderBlocked(document.querySelector('[data-recent-blocked]'), data.aggregate?.recent_blocked); + }; + + const dashboardFingerprint = (data) => { + const aggregate = data?.aggregate?.summary || {}; + const instanceSummary = Object.values(data?.instances || {}).map((entry) => ({ + id: entry?.meta?.id || '', + blocked_domains: Number(entry?.summary?.blocked_domains || 0), + ads_blocked_today: Number(entry?.summary?.ads_blocked_today || 0), + unique_domains: Number(entry?.summary?.unique_domains || 0), + })); + + return JSON.stringify({ + blocked_domains: Number(aggregate.blocked_domains || 0), + ads_blocked_today: Number(aggregate.ads_blocked_today || 0), + unique_domains: Number(aggregate.unique_domains || 0), + instances: instanceSummary, + }); + }; + + const delay = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms)); + + const monitorGravityProgress = async (baselineData, instanceLabel) => { + const baselineFingerprint = dashboardFingerprint(baselineData || {}); + const maxPolls = 24; + const pollDelayMs = 5000; + + appendActionLog(`Pi-hole meldet keinen Live-Fortschritt. Pruefe jetzt zyklisch auf erkennbare Aenderungen fuer ${instanceLabel}.`, 'info'); + + for (let attempt = 1; attempt <= maxPolls; attempt += 1) { + await delay(pollDelayMs); + appendActionLog(`Pruefung ${attempt}/${maxPolls}: aktuelle Statusdaten werden geladen ...`, 'info'); + + try { + const data = await apiCall('dashboard'); + if (!data.ok) { + throw new Error(data.error || 'API error'); + } + + renderDashboardData(data); + const nextFingerprint = dashboardFingerprint(data); + if (nextFingerprint !== baselineFingerprint) { + appendActionLog(`Erkennbare Aenderung gefunden. Listen-Update fuer ${instanceLabel} scheint abgeschlossen zu sein.`, 'success'); + return true; + } + + if (attempt % 3 === 0) { + appendActionLog('Noch keine erkennbare Aenderung in den Pi-hole Statusdaten.', 'info'); + } + } catch (err) { + appendActionLog(`Statuspruefung fehlgeschlagen: ${err.message}`, 'error'); + } + } + + appendActionLog(`Innerhalb des Beobachtungsfensters wurde keine erkennbare Aenderung gefunden. Das Update fuer ${instanceLabel} kann trotzdem weiterlaufen oder bereits ohne sichtbare Statistikaenderung beendet worden sein.`, 'error'); + return false; + }; + const renderInstances = (instances) => { const holder = document.querySelector('[data-instance-cards]'); const tpl = document.querySelector('#pihole-instance-template'); @@ -275,6 +348,7 @@ const scope = btn.closest('.pihole-instance') || document; const customInput = scope.querySelector(`[data-custom-minutes="${instance}"]`); let payload = { instance }; + let baselineData = null; if (action === 'disable') { payload.minutes = Number(minutes || 0); @@ -299,6 +373,8 @@ appendActionLog(actionLabel, 'info'); setActionLock(true, actionLabel); + baselineData = action === 'gravity' ? await apiCall('dashboard').catch(() => null) : null; + if (action === 'enable') { appendActionLog(`Aktiviere ${instance === 'all' ? 'alle Instanzen' : `Instanz ${instance}`}.`, 'info'); await apiCall('enable', payload); @@ -319,10 +395,18 @@ await apiCall('update', payload); } appendActionLog('Aktion abgeschlossen. Dashboard wird aktualisiert.', 'success'); - await loadDashboard(); - appendActionLog('Anzeige erfolgreich aktualisiert.', 'success'); + if (action === 'gravity') { + await monitorGravityProgress(baselineData, instance === 'all' ? 'alle Instanzen' : `Instanz ${instance}`); + } else { + await loadDashboard(); + appendActionLog('Anzeige erfolgreich aktualisiert.', 'success'); + } } catch (err) { appendActionLog(`Aktion fehlgeschlagen: ${err.message}`, 'error'); + if (action === 'gravity' && /timed out/i.test(err.message)) { + appendActionLog('Der Request ist in Nexus abgelaufen. Pi-hole kann intern trotzdem weiterarbeiten. Starte weiterfuehrende Statuspruefung.', 'info'); + await monitorGravityProgress(baselineData, instance === 'all' ? 'alle Instanzen' : `Instanz ${instance}`); + } } finally { setActionLock(false); } @@ -374,22 +458,7 @@ try { const data = await apiCall('dashboard'); if (!data.ok) throw new Error(data.error || 'API error'); - - const summary = data.aggregate?.summary || {}; - setText(document.querySelector('[data-summary-dns]'), fmt.format(Number(summary.dns_queries_today || 0))); - setText(document.querySelector('[data-summary-blocked]'), fmt.format(Number(summary.ads_blocked_today || 0))); - setText(document.querySelector('[data-summary-percent]'), `${Number(summary.ads_percentage_today || 0).toFixed(2)}%`); - setText(document.querySelector('[data-summary-clients]'), fmt.format(Number(summary.unique_clients || 0))); - setStatusBadge(document.querySelector('[data-summary-status]'), summary.status || 'unknown'); - setText(document.querySelector('[data-summary-last-refresh]'), `Letztes Update: ${fmtDate(data.ts)}`); - - renderInstances(data.instances || {}); - renderList(document.querySelector('[data-query-types]'), data.aggregate?.query_types, 'Keine Daten'); - renderList(document.querySelector('[data-forward-destinations]'), data.aggregate?.forward_destinations, 'Keine Daten'); - renderList(document.querySelector('[data-top-ads]'), data.aggregate?.top_ads, 'Keine Daten'); - renderList(document.querySelector('[data-top-queries]'), data.aggregate?.top_queries, 'Keine Daten'); - renderList(document.querySelector('[data-top-clients]'), data.aggregate?.query_sources, 'Keine Daten'); - renderBlocked(document.querySelector('[data-recent-blocked]'), data.aggregate?.recent_blocked); + renderDashboardData(data); const existing = page.querySelector('[data-pihole-load-error]'); if (existing) existing.remove(); } catch (err) { diff --git a/modules/pihole/pages/api.php b/modules/pihole/pages/api.php index a55b8e6..37b8292 100644 --- a/modules/pihole/pages/api.php +++ b/modules/pihole/pages/api.php @@ -513,6 +513,7 @@ if ($action === 'dashboard') { 'ads_blocked_today' => 0, 'unique_clients' => 0, 'unique_domains' => 0, + 'blocked_domains' => 0, 'queries_forwarded' => 0, 'queries_cached' => 0, 'status' => 'unknown', @@ -611,6 +612,13 @@ if ($action === 'dashboard') { 'ads_blocked_today' => (int)($queriesBlock['blocked'] ?? 0), 'unique_clients' => (int)($clientsBlock['active'] ?? $clientsBlock['total'] ?? 0), 'unique_domains' => (int)($queriesBlock['unique_domains'] ?? 0), + 'blocked_domains' => (int)( + $sum['domains_being_blocked'] + ?? $sum['gravity']['domains_being_blocked'] + ?? $sum['gravity']['domains'] + ?? $sum['gravity']['blocked'] + ?? 0 + ), 'queries_forwarded' => (int)($queriesBlock['forwarded'] ?? 0), 'queries_cached' => (int)($queriesBlock['cached'] ?? 0), 'status' => $status, @@ -768,6 +776,7 @@ if ($action === 'dashboard') { $aggregate['summary']['ads_blocked_today'] += (int)($summaryData['ads_blocked_today'] ?? 0); $aggregate['summary']['unique_clients'] += (int)($summaryData['unique_clients'] ?? 0); $aggregate['summary']['unique_domains'] += (int)($summaryData['unique_domains'] ?? 0); + $aggregate['summary']['blocked_domains'] += (int)($summaryData['blocked_domains'] ?? 0); $aggregate['summary']['queries_forwarded'] += (int)($summaryData['queries_forwarded'] ?? 0); $aggregate['summary']['queries_cached'] += (int)($summaryData['queries_cached'] ?? 0); $status = (string)($summaryData['status'] ?? 'unknown');