Modul pihole
This commit is contained in:
251
modules/pihole/assets/pihole.js
Normal file
251
modules/pihole/assets/pihole.js
Normal file
@@ -0,0 +1,251 @@
|
||||
(() => {
|
||||
const page = document.querySelector('[data-pihole-page]');
|
||||
if (!page) return;
|
||||
|
||||
const fmt = new Intl.NumberFormat('de-DE');
|
||||
const fmtDate = (ts) => {
|
||||
if (!ts) return '–';
|
||||
const d = new Date(ts * 1000);
|
||||
return d.toLocaleString('de-DE');
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const setText = (el, value) => {
|
||||
if (!el) return;
|
||||
el.textContent = value;
|
||||
};
|
||||
|
||||
const statusLabel = (status) => {
|
||||
if (status === 'enabled') return 'Aktiv';
|
||||
if (status === 'disabled') return 'Deaktiviert';
|
||||
if (status === 'partial') return 'Teilweise';
|
||||
return 'Unbekannt';
|
||||
};
|
||||
|
||||
const setStatusBadge = (el, status) => {
|
||||
if (!el) return;
|
||||
el.textContent = statusLabel(status);
|
||||
el.classList.remove('is-disabled', 'is-partial');
|
||||
if (status === 'disabled') el.classList.add('is-disabled');
|
||||
if (status === 'partial') el.classList.add('is-partial');
|
||||
};
|
||||
|
||||
const mapToList = (map, limit = 10) => {
|
||||
if (!map || typeof map !== 'object') return [];
|
||||
const entries = Object.entries(map)
|
||||
.filter(([, value]) => typeof value === 'number' || typeof value === 'string')
|
||||
.map(([key, value]) => [key, Number(value)])
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
return entries.slice(0, limit);
|
||||
};
|
||||
|
||||
const renderList = (container, map, emptyText) => {
|
||||
if (!container) return;
|
||||
const rows = mapToList(map, 10);
|
||||
container.innerHTML = '';
|
||||
if (!rows.length) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'muted';
|
||||
div.textContent = emptyText || 'Keine Daten';
|
||||
container.appendChild(div);
|
||||
return;
|
||||
}
|
||||
rows.forEach(([label, value]) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'pihole-list-row';
|
||||
row.innerHTML = `<strong>${label}</strong><span>${fmt.format(value)}</span>`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const renderBlocked = (container, list) => {
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
if (!Array.isArray(list) || !list.length) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'muted';
|
||||
div.textContent = 'Keine Daten';
|
||||
container.appendChild(div);
|
||||
return;
|
||||
}
|
||||
list.slice(0, 20).forEach((entry) => {
|
||||
const domain = entry.domain || entry;
|
||||
const instance = entry.instance || '';
|
||||
const row = document.createElement('div');
|
||||
row.className = 'pihole-blocked-row';
|
||||
row.innerHTML = `<span>${domain}</span><span class="muted">${instance}</span>`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const renderInstances = (instances) => {
|
||||
const holder = document.querySelector('[data-instance-cards]');
|
||||
const tpl = document.querySelector('#pihole-instance-template');
|
||||
if (!holder || !tpl) return;
|
||||
holder.innerHTML = '';
|
||||
|
||||
Object.values(instances).forEach((entry) => {
|
||||
const node = tpl.content.cloneNode(true);
|
||||
const root = node.querySelector('[data-instance]');
|
||||
if (!root) return;
|
||||
root.dataset.instance = entry.meta.id;
|
||||
|
||||
const summary = entry.summary || {};
|
||||
setText(root.querySelector('[data-instance-name]'), entry.meta.name || entry.meta.id);
|
||||
setText(root.querySelector('[data-instance-url]'), entry.meta.url || '');
|
||||
setText(root.querySelector('[data-instance-dns]'), fmt.format(Number(summary.dns_queries_today || 0)));
|
||||
setText(root.querySelector('[data-instance-ads]'), fmt.format(Number(summary.ads_blocked_today || 0)));
|
||||
const percent = summary.ads_percentage_today || summary.ads_percentage || 0;
|
||||
setText(root.querySelector('[data-instance-percent]'), `${Number(percent).toFixed(2)}%`);
|
||||
setStatusBadge(root.querySelector('[data-instance-status]'), summary.status || 'unknown');
|
||||
|
||||
const actions = root.querySelectorAll('[data-instance]');
|
||||
actions.forEach((btn) => {
|
||||
if (btn.dataset.instance === '') {
|
||||
btn.dataset.instance = entry.meta.id;
|
||||
}
|
||||
});
|
||||
const customInput = root.querySelector('[data-custom-minutes]');
|
||||
if (customInput) {
|
||||
customInput.dataset.customMinutes = entry.meta.id;
|
||||
}
|
||||
|
||||
const updateEl = root.querySelector('[data-instance-update]');
|
||||
if (entry.updates && entry.updates.available) {
|
||||
updateEl.textContent = 'Updates verfuegbar';
|
||||
} else {
|
||||
updateEl.textContent = 'Keine Updates erkannt';
|
||||
}
|
||||
|
||||
holder.appendChild(node);
|
||||
});
|
||||
};
|
||||
|
||||
const bindActionButtons = () => {
|
||||
document.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const action = btn.dataset.action;
|
||||
const instance = btn.dataset.instance || 'all';
|
||||
const minutes = btn.dataset.minutes;
|
||||
const scope = btn.closest('.pihole-instance') || document;
|
||||
const customInput = scope.querySelector(`[data-custom-minutes="${instance}"]`);
|
||||
let payload = { instance };
|
||||
|
||||
if (action === 'disable') {
|
||||
payload.minutes = Number(minutes || 0);
|
||||
}
|
||||
if (action === 'disable-custom') {
|
||||
payload.minutes = Number(customInput?.value || 0);
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === 'enable') {
|
||||
await apiCall('enable', payload);
|
||||
} else if (action === 'disable' || action === 'disable-custom') {
|
||||
if (!payload.minutes || payload.minutes <= 0) {
|
||||
alert('Bitte Minuten angeben.');
|
||||
return;
|
||||
}
|
||||
await apiCall('disable', payload);
|
||||
} else if (action === 'gravity') {
|
||||
await apiCall('gravity', payload);
|
||||
const status = document.querySelector('[data-list-update-status]');
|
||||
if (status) status.textContent = 'Listen-Update gestartet.';
|
||||
} else if (action === 'update') {
|
||||
await apiCall('update', payload);
|
||||
}
|
||||
await loadDashboard();
|
||||
} catch (err) {
|
||||
alert(`Aktion fehlgeschlagen: ${err.message}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const bindForms = () => {
|
||||
const domainForm = document.querySelector('[data-domain-form]');
|
||||
if (domainForm) {
|
||||
domainForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(domainForm);
|
||||
const type = String(formData.get('type') || 'block');
|
||||
const domain = String(formData.get('domain') || '').trim();
|
||||
const status = document.querySelector('[data-domain-status]');
|
||||
if (!domain) return;
|
||||
try {
|
||||
await apiCall('domain_add', { type, domain, instance: 'primary' });
|
||||
if (status) status.textContent = `Domain hinzugefuegt: ${domain}`;
|
||||
domainForm.reset();
|
||||
} catch (err) {
|
||||
if (status) status.textContent = `Fehler: ${err.message}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const adlistForm = document.querySelector('[data-adlist-form]');
|
||||
if (adlistForm) {
|
||||
adlistForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(adlistForm);
|
||||
const url = String(formData.get('url') || '').trim();
|
||||
const status = document.querySelector('[data-adlist-status]');
|
||||
if (!url) return;
|
||||
try {
|
||||
await apiCall('adlist_add', { url, instance: 'primary' });
|
||||
if (status) status.textContent = `Adlist hinzugefuegt: ${url}`;
|
||||
adlistForm.reset();
|
||||
} catch (err) {
|
||||
if (status) status.textContent = `Fehler: ${err.message}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loadDashboard = async () => {
|
||||
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);
|
||||
} catch (err) {
|
||||
const message = document.createElement('div');
|
||||
message.className = 'card';
|
||||
message.style.marginTop = '1rem';
|
||||
message.textContent = `Fehler beim Laden der Pi-hole Daten: ${err.message}`;
|
||||
page.appendChild(message);
|
||||
}
|
||||
};
|
||||
|
||||
bindActionButtons();
|
||||
bindForms();
|
||||
loadDashboard();
|
||||
})();
|
||||
Reference in New Issue
Block a user