diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c23d97c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.claude diff --git a/README.md b/README.md index 538411c..e7be22a 100644 --- a/README.md +++ b/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` diff --git a/pihole-client-name-manager.user.js b/pihole-client-name-manager.user.js new file mode 100644 index 0000000..7f878aa --- /dev/null +++ b/pihole-client-name-manager.user.js @@ -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 = ` +
${safeIp}