Neu: Pi-hole Client Name Manager
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.claude
|
||||
23
README.md
23
README.md
@@ -185,6 +185,29 @@ Blockiert nervige Popup-Werbung auf muchohentai.com, die sich bei jedem Klick ö
|
||||
|
||||
---
|
||||
|
||||
### 11. `pihole-client-name-manager.user.js`
|
||||
|
||||
**Beschreibung:**
|
||||
Ersetzt Client-Namen in den "Top Clients" Tabellen des Pi-hole Admin-Dashboards durch eigene Namen und bietet eine Management-Oberfläche zur Verwaltung.
|
||||
|
||||
**Funktionen:**
|
||||
- Automatische Ersetzung von Client-Namen in "Top Clients (total)" und "Top Clients (blocked only)"
|
||||
- Management-Interface unter Settings → "Client Names" im Sidebar
|
||||
- Vollständige Client-Liste mit MAC-Adressen, Hostnamen und Custom-Namen
|
||||
- Filter-Funktion zum schnellen Finden von Clients
|
||||
- Speichert Namen persistent mit Tampermonkey Storage (GM_setValue)
|
||||
- Original-Namen werden als Tooltip beim Hover angezeigt
|
||||
- Pi-hole API Integration (`/api/clients`) für vollständige Client-Liste
|
||||
- Fallback auf Seiten-Scraping wenn API nicht verfügbar
|
||||
- URL-Whitelist Verwaltung über Tampermonkey-Menü
|
||||
- "URL zur Whitelist hinzufügen" - fügt aktuelle URL hinzu
|
||||
- "Whitelist verwalten" - zeigt alle URLs und ermöglicht Löschen
|
||||
- Automatische Erkennung von MAC → Hostname Zuordnung
|
||||
- Kein Flackern durch intelligentes Update-System mit data-Attributen
|
||||
- Update-Intervall: 500ms für schnelle Reaktion auf AJAX-Updates
|
||||
|
||||
---
|
||||
|
||||
## Übersicht der enthaltenen UserStyles
|
||||
|
||||
### 1. `myanimelist-tweaks.user.css`
|
||||
|
||||
712
pihole-client-name-manager.user.js
Normal file
712
pihole-client-name-manager.user.js
Normal file
@@ -0,0 +1,712 @@
|
||||
// ==UserScript==
|
||||
// @name Pi-hole Client Name Manager
|
||||
// @namespace https://git.ponywave.de/Akamaru/Userscripts
|
||||
// @version 1.0
|
||||
// @description Ersetzt Client-Namen in Pi-hole Admin Dashboard und bietet eine Management-Oberfläche
|
||||
// @author Akamaru
|
||||
// @match http://pi.hole/admin/*
|
||||
// @match http://*/*
|
||||
// @grant GM_getValue
|
||||
// @grant GM_setValue
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @grant GM_registerMenuCommand
|
||||
// @run-at document-idle
|
||||
// @updateURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/pihole-client-name-manager.user.js
|
||||
// @downloadURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/pihole-client-name-manager.user.js
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Konfiguration
|
||||
const CONFIG = {
|
||||
defaultWhitelistedUrls: [
|
||||
'http://pi.hole/admin/'
|
||||
],
|
||||
storageKeys: {
|
||||
clientNames: 'pihole_custom_client_names',
|
||||
whitelistUrls: 'pihole_whitelist_urls'
|
||||
},
|
||||
apiEndpoints: {
|
||||
clients: '/api/clients'
|
||||
}
|
||||
};
|
||||
|
||||
// Lade Whitelist-URLs
|
||||
function loadWhitelist() {
|
||||
const stored = GM_getValue(CONFIG.storageKeys.whitelistUrls, '[]');
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
return [...CONFIG.defaultWhitelistedUrls, ...parsed];
|
||||
} catch (e) {
|
||||
return [...CONFIG.defaultWhitelistedUrls];
|
||||
}
|
||||
}
|
||||
|
||||
// Speichere Whitelist-URLs (ohne Default-URLs)
|
||||
function saveWhitelist(urls) {
|
||||
// Filtere Default-URLs raus
|
||||
const customUrls = urls.filter(url => !CONFIG.defaultWhitelistedUrls.includes(url));
|
||||
GM_setValue(CONFIG.storageKeys.whitelistUrls, JSON.stringify(customUrls));
|
||||
}
|
||||
|
||||
// Prüfe ob aktuelle URL in Whitelist ist
|
||||
function isWhitelisted() {
|
||||
const currentOrigin = window.location.origin;
|
||||
const currentPath = window.location.pathname;
|
||||
const fullUrl = currentOrigin + currentPath;
|
||||
|
||||
const whitelist = loadWhitelist();
|
||||
|
||||
// Prüfe ob eine URL aus der Whitelist matcht
|
||||
return whitelist.some(url => {
|
||||
// Entferne trailing /* falls vorhanden
|
||||
const cleanUrl = url.replace(/\/\*$/, '');
|
||||
return fullUrl.startsWith(cleanUrl);
|
||||
});
|
||||
}
|
||||
|
||||
// Füge aktuelle URL zur Whitelist hinzu
|
||||
function addCurrentUrlToWhitelist() {
|
||||
const currentUrl = window.location.origin + window.location.pathname;
|
||||
const whitelist = loadWhitelist();
|
||||
|
||||
if (!whitelist.includes(currentUrl)) {
|
||||
whitelist.push(currentUrl);
|
||||
saveWhitelist(whitelist);
|
||||
alert(`URL zur Whitelist hinzugefügt:\n${currentUrl}\n\nBitte Seite neu laden.`);
|
||||
} else {
|
||||
alert('Diese URL ist bereits in der Whitelist.');
|
||||
}
|
||||
}
|
||||
|
||||
// Verwalte Whitelist über Dialog
|
||||
function manageWhitelist() {
|
||||
const whitelist = loadWhitelist();
|
||||
const customUrls = whitelist.filter(url => !CONFIG.defaultWhitelistedUrls.includes(url));
|
||||
|
||||
let message = 'Pi-hole Client Manager - Whitelist\n\n';
|
||||
message += 'Standard-URLs (nicht löschbar):\n';
|
||||
CONFIG.defaultWhitelistedUrls.forEach((url, i) => {
|
||||
message += ` ${i + 1}. ${url}\n`;
|
||||
});
|
||||
|
||||
if (customUrls.length > 0) {
|
||||
message += '\nBenutzerdefinierte URLs:\n';
|
||||
customUrls.forEach((url, i) => {
|
||||
message += ` ${i + 1}. ${url}\n`;
|
||||
});
|
||||
message += '\n\nURL-Nummer eingeben zum Löschen, oder "0" zum Abbrechen:';
|
||||
|
||||
const input = prompt(message);
|
||||
if (input && input !== '0') {
|
||||
const index = parseInt(input) - 1;
|
||||
if (index >= 0 && index < customUrls.length) {
|
||||
customUrls.splice(index, 1);
|
||||
saveWhitelist([...CONFIG.defaultWhitelistedUrls, ...customUrls]);
|
||||
alert('URL entfernt! Bitte Seite neu laden.');
|
||||
} else {
|
||||
alert('Ungültige Nummer.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message += '\nKeine benutzerdefinierten URLs vorhanden.\n\n';
|
||||
message += 'Verwende "URL zur Whitelist hinzufügen" im Tampermonkey-Menü.';
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe ob Seite Pi-hole Admin ist (Fallback)
|
||||
function isPiHoleAdmin() {
|
||||
return document.querySelector('.sidebar-menu') !== null &&
|
||||
document.querySelector('#client-frequency') !== null;
|
||||
}
|
||||
|
||||
// Speichere Custom-Namen
|
||||
function saveCustomNames(names) {
|
||||
GM_setValue(CONFIG.storageKeys.clientNames, JSON.stringify(names));
|
||||
}
|
||||
|
||||
// Lade Custom-Namen
|
||||
function loadCustomNames() {
|
||||
try {
|
||||
const data = GM_getValue(CONFIG.storageKeys.clientNames, '{}');
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Custom-Namen:', e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Speichere Client-Mapping (MAC/IP -> Hostname Zuordnung)
|
||||
let clientMapping = {};
|
||||
|
||||
function updateClientMapping(clients) {
|
||||
// Erstelle Mapping: Hostname -> MAC/IP und IP -> MAC
|
||||
clientMapping = {};
|
||||
clients.forEach(client => {
|
||||
const identifier = client.ip; // MAC oder IP
|
||||
const hostname = client.name;
|
||||
|
||||
// Mapping: Hostname -> Identifier (mit und ohne .fritz.box)
|
||||
if (hostname) {
|
||||
const lowerHostname = hostname.toLowerCase();
|
||||
clientMapping[lowerHostname] = identifier;
|
||||
|
||||
// Auch ohne Domain-Suffix speichern
|
||||
const shortName = hostname.split('.')[0].toLowerCase();
|
||||
if (shortName !== lowerHostname) {
|
||||
clientMapping[shortName] = identifier;
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn es eine IP-Adresse im Namen gibt, auch diese mappen
|
||||
const ipMatch = hostname.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/);
|
||||
if (ipMatch) {
|
||||
clientMapping[ipMatch[0]] = identifier;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function findIdentifierForClient(hostnameOrIp) {
|
||||
// Versuche direkt zu matchen
|
||||
if (clientMapping[hostnameOrIp]) {
|
||||
return clientMapping[hostnameOrIp];
|
||||
}
|
||||
|
||||
// Versuche case-insensitive
|
||||
const lower = hostnameOrIp.toLowerCase();
|
||||
if (clientMapping[lower]) {
|
||||
return clientMapping[lower];
|
||||
}
|
||||
|
||||
// Versuche ohne Domain-Suffix
|
||||
const shortName = hostnameOrIp.split('.')[0].toLowerCase();
|
||||
if (clientMapping[shortName]) {
|
||||
return clientMapping[shortName];
|
||||
}
|
||||
|
||||
return hostnameOrIp; // Fallback
|
||||
}
|
||||
|
||||
// Hole Clients von API
|
||||
function fetchClientsFromAPI() {
|
||||
return new Promise((resolve, reject) => {
|
||||
GM_xmlhttpRequest({
|
||||
method: 'GET',
|
||||
url: window.location.origin + CONFIG.apiEndpoints.clients,
|
||||
onload: function(response) {
|
||||
try {
|
||||
if (response.status === 200) {
|
||||
const data = JSON.parse(response.responseText);
|
||||
|
||||
// Prüfe verschiedene mögliche Datenstrukturen
|
||||
let clientList = [];
|
||||
if (data.clients && Array.isArray(data.clients)) {
|
||||
clientList = data.clients;
|
||||
} else if (Array.isArray(data)) {
|
||||
clientList = data;
|
||||
} else if (data.data && Array.isArray(data.data)) {
|
||||
clientList = data.data;
|
||||
}
|
||||
|
||||
// Normalisiere Client-Objekte
|
||||
clientList = clientList.map(client => {
|
||||
// Pi-hole API v6 nutzt 'client' für MAC/IP, 'name' für Hostname
|
||||
const identifier = client.ip || client.client || client.address || client.id;
|
||||
const displayName = client.name || client.hostname || client.comment || identifier || 'Unknown';
|
||||
|
||||
return {
|
||||
ip: String(identifier || 'unknown'),
|
||||
name: String(displayName),
|
||||
comment: client.comment || ''
|
||||
};
|
||||
}).filter(client => client.ip !== 'unknown');
|
||||
|
||||
if (clientList.length > 0) {
|
||||
resolve(clientList);
|
||||
} else {
|
||||
resolve(getClientsFromPage());
|
||||
}
|
||||
} else if (response.status === 401) {
|
||||
resolve(getClientsFromPage());
|
||||
} else {
|
||||
resolve(getClientsFromPage());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Pi-hole Client Manager: Fehler beim Parsen der API-Antwort:', e);
|
||||
resolve(getClientsFromPage());
|
||||
}
|
||||
},
|
||||
onerror: function(error) {
|
||||
console.error('Pi-hole Client Manager: API Request fehlgeschlagen:', error);
|
||||
resolve(getClientsFromPage());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: Hole Clients von der Seite
|
||||
function getClientsFromPage() {
|
||||
const clients = [];
|
||||
const tables = [
|
||||
document.querySelector('#client-frequency table'),
|
||||
document.querySelector('#client-frequency-blocked table')
|
||||
];
|
||||
|
||||
tables.forEach(table => {
|
||||
if (!table) return;
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const link = row.querySelector('td:first-child a');
|
||||
if (link) {
|
||||
const href = link.getAttribute('href');
|
||||
const ipMatch = href.match(/client_ip=([^&]+)/);
|
||||
if (ipMatch) {
|
||||
const ip = ipMatch[1];
|
||||
const name = link.textContent.trim();
|
||||
// Verhindere Duplikate
|
||||
if (!clients.find(c => c.ip === ip)) {
|
||||
clients.push({
|
||||
ip: ip,
|
||||
name: name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
// Ersetze Client-Namen in Tabellen
|
||||
function replaceClientNames() {
|
||||
const customNames = loadCustomNames();
|
||||
|
||||
const tables = [
|
||||
document.querySelector('#client-frequency table'),
|
||||
document.querySelector('#client-frequency-blocked table')
|
||||
];
|
||||
|
||||
let replacedCount = 0;
|
||||
|
||||
tables.forEach(table => {
|
||||
if (!table) return;
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const link = row.querySelector('td:first-child a');
|
||||
if (link) {
|
||||
const href = link.getAttribute('href');
|
||||
const ipMatch = href.match(/client_ip=([^&]+)/);
|
||||
if (ipMatch) {
|
||||
const ip = ipMatch[1];
|
||||
|
||||
// Prüfe ob Link bereits bearbeitet wurde
|
||||
let originalName;
|
||||
if (link.hasAttribute('data-original-name')) {
|
||||
// Link wurde bereits ersetzt, hole Original-Namen aus data-Attribut
|
||||
originalName = link.getAttribute('data-original-name');
|
||||
} else {
|
||||
// Erster Durchlauf, hole Original-Namen aus Text
|
||||
originalName = link.textContent.trim();
|
||||
// Speichere Original-Namen für spätere Durchläufe
|
||||
link.setAttribute('data-original-name', originalName);
|
||||
}
|
||||
|
||||
// Versuche den Identifier (MAC) für diese IP/Hostname zu finden
|
||||
const identifierFromIp = findIdentifierForClient(ip);
|
||||
const identifierFromName = findIdentifierForClient(originalName);
|
||||
|
||||
// Priorität: MAC-Adresse (enthält :) über IP-Adresse
|
||||
let identifier;
|
||||
if (identifierFromName && identifierFromName.includes(':')) {
|
||||
identifier = identifierFromName; // MAC-Adresse vom Hostname
|
||||
} else if (identifierFromIp && identifierFromIp.includes(':')) {
|
||||
identifier = identifierFromIp; // MAC-Adresse von IP
|
||||
} else {
|
||||
identifier = identifierFromName || identifierFromIp; // Fallback
|
||||
}
|
||||
|
||||
// Prüfe ob ein Custom-Name existiert (mit IP, Hostname oder MAC)
|
||||
let customName = customNames[identifier] || customNames[originalName] || customNames[ip];
|
||||
|
||||
if (customName) {
|
||||
// Setze Custom-Namen nur wenn er sich unterscheidet
|
||||
if (link.textContent !== customName) {
|
||||
link.textContent = customName;
|
||||
replacedCount++;
|
||||
}
|
||||
// Füge Tooltip mit Original-Namen hinzu
|
||||
link.title = 'Original: ' + originalName + '\nIP: ' + ip;
|
||||
link.style.cursor = 'help';
|
||||
} else {
|
||||
// Kein Custom-Name mehr vorhanden, stelle Original wieder her
|
||||
if (link.textContent !== originalName) {
|
||||
link.textContent = originalName;
|
||||
}
|
||||
// Entferne Tooltip
|
||||
link.removeAttribute('title');
|
||||
link.style.cursor = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Erstelle Management-Modal
|
||||
function createManagementModal() {
|
||||
// Modal HTML
|
||||
const modalHtml = `
|
||||
<div id="pihole-client-manager-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-edit"></i> Client Name Manager
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="client-manager-loading" class="text-center" style="padding: 20px;">
|
||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||
<p>Lade Clients...</p>
|
||||
</div>
|
||||
<div id="client-manager-content" style="display: none;">
|
||||
<div class="alert alert-info">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
Gib für jeden Client einen Custom-Namen ein. Leere Felder verwenden den Original-Namen.
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" id="client-filter" class="form-control" placeholder="Filter nach IP oder Name...">
|
||||
</div>
|
||||
<div style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-striped table-bordered" id="client-manager-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%;">MAC/IP</th>
|
||||
<th style="width: 35%;">Hostname</th>
|
||||
<th style="width: 40%;">Custom-Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="client-list">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="client-manager-error" style="display: none;">
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<span id="error-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
<i class="fa fa-times"></i> Schließen
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" id="reset-names-btn">
|
||||
<i class="fa fa-undo"></i> Alle zurücksetzen
|
||||
</button>
|
||||
<button type="button" class="btn btn-success" id="save-names-btn">
|
||||
<i class="fa fa-save"></i> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Füge Modal zum Body hinzu
|
||||
const modalContainer = document.createElement('div');
|
||||
modalContainer.innerHTML = modalHtml;
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
// Event Listeners
|
||||
document.getElementById('save-names-btn').addEventListener('click', saveNamesFromModal);
|
||||
document.getElementById('reset-names-btn').addEventListener('click', resetAllNames);
|
||||
document.getElementById('client-filter').addEventListener('input', filterClientTable);
|
||||
|
||||
// Lade Clients wenn Modal geöffnet wird
|
||||
$('#pihole-client-manager-modal').on('show.bs.modal', function() {
|
||||
loadClientsIntoModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Lade Clients in Modal
|
||||
async function loadClientsIntoModal() {
|
||||
const loadingDiv = document.getElementById('client-manager-loading');
|
||||
const contentDiv = document.getElementById('client-manager-content');
|
||||
const errorDiv = document.getElementById('client-manager-error');
|
||||
|
||||
loadingDiv.style.display = 'block';
|
||||
contentDiv.style.display = 'none';
|
||||
errorDiv.style.display = 'none';
|
||||
|
||||
try {
|
||||
const clients = await fetchClientsFromAPI();
|
||||
const customNames = loadCustomNames();
|
||||
const tbody = document.getElementById('client-list');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (clients.length === 0) {
|
||||
throw new Error('Keine Clients gefunden');
|
||||
}
|
||||
|
||||
// Aktualisiere Client-Mapping für Namen-Ersetzung
|
||||
updateClientMapping(clients);
|
||||
|
||||
// Sortiere nach Name (alphabetisch)
|
||||
clients.sort((a, b) => {
|
||||
// Sortiere nach Name, dann nach IP/MAC
|
||||
const nameA = (a.name || '').toLowerCase();
|
||||
const nameB = (b.name || '').toLowerCase();
|
||||
|
||||
if (nameA !== nameB) {
|
||||
return nameA.localeCompare(nameB);
|
||||
}
|
||||
|
||||
// Falls Namen gleich, sortiere nach IP/MAC
|
||||
const ipA = String(a.ip || '');
|
||||
const ipB = String(b.ip || '');
|
||||
return ipA.localeCompare(ipB);
|
||||
});
|
||||
|
||||
clients.forEach(client => {
|
||||
// Sicherheitsprüfung
|
||||
if (!client.ip) return;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
const safeIp = String(client.ip || '').replace(/[<>&"']/g, '');
|
||||
const safeName = String(client.name || 'Unknown').replace(/[<>&"']/g, '');
|
||||
const safeComment = String(client.comment || '').replace(/[<>&"']/g, '');
|
||||
const customName = customNames[client.ip] || '';
|
||||
const safeCustomName = String(customName).replace(/"/g, '"');
|
||||
|
||||
// Zeige Kommentar in Tooltip falls vorhanden
|
||||
const nameDisplay = safeComment ?
|
||||
`<span title="${safeComment}">${safeName}</span>` :
|
||||
safeName;
|
||||
|
||||
row.setAttribute('data-ip', safeIp);
|
||||
row.setAttribute('data-name', safeName);
|
||||
|
||||
row.innerHTML = `
|
||||
<td><code style="font-size: 11px;">${safeIp}</code></td>
|
||||
<td>${nameDisplay}</td>
|
||||
<td>
|
||||
<input type="text"
|
||||
class="form-control input-sm custom-name-input"
|
||||
data-ip="${safeIp}"
|
||||
value="${safeCustomName}"
|
||||
placeholder="Custom-Name eingeben...">
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
loadingDiv.style.display = 'none';
|
||||
contentDiv.style.display = 'block';
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Clients:', error);
|
||||
document.getElementById('error-message').textContent = error.message;
|
||||
loadingDiv.style.display = 'none';
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Filter Client-Tabelle
|
||||
function filterClientTable() {
|
||||
const filter = document.getElementById('client-filter').value.toLowerCase();
|
||||
const rows = document.querySelectorAll('#client-list tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const ip = row.getAttribute('data-ip').toLowerCase();
|
||||
const name = row.getAttribute('data-name').toLowerCase();
|
||||
const customInput = row.querySelector('.custom-name-input');
|
||||
const customName = customInput ? customInput.value.toLowerCase() : '';
|
||||
|
||||
if (ip.includes(filter) || name.includes(filter) || customName.includes(filter)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Speichere Namen aus Modal
|
||||
function saveNamesFromModal() {
|
||||
const inputs = document.querySelectorAll('.custom-name-input');
|
||||
const customNames = loadCustomNames();
|
||||
|
||||
inputs.forEach(input => {
|
||||
const ip = input.getAttribute('data-ip');
|
||||
const value = input.value.trim();
|
||||
|
||||
if (value) {
|
||||
customNames[ip] = value;
|
||||
} else {
|
||||
delete customNames[ip];
|
||||
}
|
||||
});
|
||||
|
||||
saveCustomNames(customNames);
|
||||
replaceClientNames();
|
||||
|
||||
// Zeige Erfolgs-Nachricht
|
||||
const saveBtn = document.getElementById('save-names-btn');
|
||||
const originalHtml = saveBtn.innerHTML;
|
||||
saveBtn.innerHTML = '<i class="fa fa-check"></i> Gespeichert!';
|
||||
saveBtn.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
saveBtn.innerHTML = originalHtml;
|
||||
saveBtn.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Setze alle Namen zurück
|
||||
function resetAllNames() {
|
||||
if (confirm('Möchtest du wirklich alle Custom-Namen löschen?')) {
|
||||
saveCustomNames({});
|
||||
replaceClientNames();
|
||||
|
||||
// Leere alle Inputs
|
||||
const inputs = document.querySelectorAll('.custom-name-input');
|
||||
inputs.forEach(input => input.value = '');
|
||||
|
||||
// Zeige Bestätigung
|
||||
const resetBtn = document.getElementById('reset-names-btn');
|
||||
const originalHtml = resetBtn.innerHTML;
|
||||
resetBtn.innerHTML = '<i class="fa fa-check"></i> Zurückgesetzt!';
|
||||
resetBtn.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
resetBtn.innerHTML = originalHtml;
|
||||
resetBtn.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Füge Menüpunkt zum Sidebar hinzu
|
||||
function addMenuEntry() {
|
||||
const toolsMenu = document.querySelector('.menu-system.treeview ul.treeview-menu');
|
||||
if (!toolsMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob Eintrag bereits existiert
|
||||
if (document.querySelector('#client-manager-menu-item')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuItem = document.createElement('li');
|
||||
menuItem.id = 'client-manager-menu-item';
|
||||
menuItem.innerHTML = `
|
||||
<a href="#" id="open-client-manager">
|
||||
<i class="fa-fw menu-icon fa-solid fa-address-card"></i>
|
||||
<span>Client Names</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
// Füge nach "Network" Eintrag ein
|
||||
const networkItem = Array.from(toolsMenu.querySelectorAll('li')).find(li =>
|
||||
li.textContent.includes('Network')
|
||||
);
|
||||
|
||||
if (networkItem) {
|
||||
networkItem.after(menuItem);
|
||||
} else {
|
||||
toolsMenu.appendChild(menuItem);
|
||||
}
|
||||
|
||||
// Event Listener für Menüpunkt
|
||||
document.getElementById('open-client-manager').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('#pihole-client-manager-modal').modal('show');
|
||||
});
|
||||
}
|
||||
|
||||
// Beobachte Änderungen an Tabellen
|
||||
function observeTableChanges() {
|
||||
const targetNode = document.querySelector('body');
|
||||
if (!targetNode) return;
|
||||
|
||||
const config = { childList: true, subtree: true };
|
||||
|
||||
const callback = function(mutationsList, observer) {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
// Prüfe ob Tabellen aktualisiert wurden
|
||||
const hasClientTable = mutation.target.querySelector('#client-frequency table') ||
|
||||
mutation.target.querySelector('#client-frequency-blocked table');
|
||||
if (hasClientTable) {
|
||||
replaceClientNames();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(callback);
|
||||
observer.observe(targetNode, config);
|
||||
}
|
||||
|
||||
// Registriere Tampermonkey-Menübefehle
|
||||
GM_registerMenuCommand('URL zur Whitelist hinzufügen', addCurrentUrlToWhitelist);
|
||||
GM_registerMenuCommand('Whitelist verwalten', manageWhitelist);
|
||||
|
||||
// Initialisierung
|
||||
async function init() {
|
||||
// Prüfe Whitelist - Script läuft nur auf whitelisteten URLs
|
||||
if (!isWhitelisted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob es eine Pi-hole Admin-Seite ist
|
||||
if (!isPiHoleAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Warte bis Seite vollständig geladen ist
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
return;
|
||||
}
|
||||
|
||||
// Warte auf jQuery und Bootstrap
|
||||
if (typeof jQuery === 'undefined' || typeof jQuery.fn.modal === 'undefined') {
|
||||
setTimeout(init, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lade Client-Liste und erstelle Mapping
|
||||
try {
|
||||
const clients = await fetchClientsFromAPI();
|
||||
updateClientMapping(clients);
|
||||
} catch (e) {
|
||||
console.error('Pi-hole Client Manager: Konnte Client-Mapping nicht initialisieren:', e);
|
||||
}
|
||||
|
||||
// Ersetze Namen in Tabellen
|
||||
replaceClientNames();
|
||||
|
||||
// Füge Modal hinzu
|
||||
createManagementModal();
|
||||
|
||||
// Füge Menüpunkt hinzu
|
||||
addMenuEntry();
|
||||
|
||||
// Beobachte Änderungen
|
||||
observeTableChanges();
|
||||
|
||||
// Aktualisiere Namen periodisch (falls Tabellen neu geladen werden)
|
||||
// Kurzes Intervall für schnelle Reaktion auf AJAX-Updates
|
||||
setInterval(replaceClientNames, 500);
|
||||
}
|
||||
|
||||
// Starte Script
|
||||
init();
|
||||
})();
|
||||
Reference in New Issue
Block a user