document.addEventListener('DOMContentLoaded', function() { const serviceForm = document.getElementById('add-service-form'); const intervalSelect = document.getElementById('check-interval'); const notifyOnlineCheckbox = document.getElementById('notify-online'); const exportBtn = document.getElementById('export-services'); const importBtn = document.getElementById('import-services'); const importFileInput = document.getElementById('import-file'); const servicesListStat = document.getElementById('services-list-stat'); const emptyState = document.getElementById('empty-state'); const searchInput = document.getElementById('services-search'); const MAX_POPUP_SERVERS = 8; let currentChart = null; // NEU: Discord Webhook Input const discordWebhookInput = document.getElementById('discord-webhook-url'); // NEU: Telegram Inputs const telegramBotTokenInput = document.getElementById('telegram-bot-token'); const telegramChatIdInput = document.getElementById('telegram-chat-id'); // --- Tab-Logik --- const tabButtons = document.querySelectorAll('.tab-btn'); const tabPanels = document.querySelectorAll('.tab-panel'); tabButtons.forEach(button => { button.addEventListener('click', () => { const targetTab = button.getAttribute('data-tab'); tabButtons.forEach(btn => btn.classList.remove('active')); tabPanels.forEach(panel => panel.classList.remove('active')); button.classList.add('active'); document.getElementById(`${targetTab}-panel`).classList.add('active'); }); }); // --- Utilities --- function getAllData(callback) { chrome.storage.sync.get({ services: [], popupServers: [] }, function(syncData) { chrome.storage.local.get({ serviceStatus: {}, history: {} }, function(localData) { callback(syncData.services || [], syncData.popupServers || [], localData.serviceStatus || {}, localData.history || {}); }); }); } // --- NEU: Popup-Server-Auswahl rendern --- function renderPopupServerSelection(allServices) { const container = document.getElementById('popup-server-list'); const infoElement = document.getElementById('selection-info'); container.innerHTML = ''; // Leeren if (allServices.length === 0) { infoElement.textContent = 'Bitte fügen Sie zuerst Server hinzu.'; return; } chrome.storage.sync.get({ popupServers: [] }, (data) => { const selectedServers = data.popupServers || []; // Info-Text aktualisieren infoElement.textContent = `${selectedServers.length} von ${MAX_POPUP_SERVERS} Servern für das Popup ausgewählt.`; allServices.forEach(service => { const serverItem = document.createElement('div'); serverItem.className = 'popup-server-item'; const isSelected = selectedServers.some(s => s.name === service.name && s.adresse === service.adresse); serverItem.innerHTML = `
${escapeHtml(service.name)}
${escapeHtml(service.adresse || '')}
`; const checkbox = serverItem.querySelector('input[type="checkbox"]'); checkbox.addEventListener('change', () => { updatePopupServerSelection(service, checkbox.checked, allServices); }); container.appendChild(serverItem); }); }); } // --- NEU: Auswahl aktualisieren --- function updatePopupServerSelection(service, isSelected, allServices) { chrome.storage.sync.get({ popupServers: [] }, (data) => { let selectedServers = data.popupServers || []; if (isSelected) { // Verhindere, dass mehr als MAX_POPUP_SERVERS ausgewählt werden if (selectedServers.length >= MAX_POPUP_SERVERS) { alert(`Sie können maximal ${MAX_POPUP_SERVERS} Server auswählen.`); // Finde die Checkbox und setze den Haken zurück const checkboxId = `popup-server-${service.name.replace(/\s+/g, '-')}`; const checkbox = document.getElementById(checkboxId); if(checkbox) checkbox.checked = false; return; } selectedServers.push(service); } else { selectedServers = selectedServers.filter(s => !(s.name === service.name && s.adresse === service.adresse)); } chrome.storage.sync.set({ popupServers: selectedServers }, () => { // Info-Text und Checkboxen aktualisieren renderPopupServerSelection(allServices); }); }); } // --- Services rendern --- function renderServices(filter = '') { getAllData((services, popupServers, statuses, history) => { servicesListStat.innerHTML = ''; const filtered = services.filter(s => s.name.toLowerCase().includes(filter.toLowerCase())); if (filtered.length === 0) { emptyState.style.display = services.length === 0 ? 'block' : 'none'; if (services.length === 0) return; } else { emptyState.style.display = 'none'; } filtered.forEach((service, index) => { const statusObj = statuses[service.name] || {}; const st = statusObj.status || 'unknown'; const resp = typeof statusObj.responseTime === 'number' ? `${statusObj.responseTime} ms` : ''; const li = document.createElement('li'); li.className = 'service-stat-item'; li.dataset.index = index; li.innerHTML = `
${escapeHtml(service.name)}
${escapeHtml(service.adresse || '')}
${resp}
`; li.querySelector('.service-name').addEventListener('click', () => { showStatsForService(service); highlightSelection(li); }); li.querySelector('.service-name').addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); li.querySelector('.service-name').click(); } }); li.querySelector('.icon-btn.edit').addEventListener('click', (e) => { e.stopPropagation(); enterEditMode(li, service, services); }); li.querySelector('.icon-btn.delete').addEventListener('click', (e) => { e.stopPropagation(); if (confirm(`Server "${service.name}" wirklich löschen?`)) { services.splice(index, 1); chrome.storage.sync.set({ services }, () => { renderServices(searchInput.value.trim()); renderPopupServerSelection(services); // Auch die Auswahl neu rendern }); } }); servicesListStat.appendChild(li); }); }); } function highlightSelection(itemEl) { document.querySelectorAll('.service-stat-item').forEach(it => it.classList.remove('selected')); itemEl.classList.add('selected'); } function enterEditMode(li, service, allServices) { li.classList.add('editing'); li.innerHTML = `
`; const saveBtn = li.querySelector('.btn-save'); const cancelBtn = li.querySelector('.btn-cancel'); cancelBtn.addEventListener('click', (ev) => { ev.stopPropagation(); renderServices(searchInput.value.trim()); }); saveBtn.addEventListener('click', (ev) => { ev.stopPropagation(); const newName = li.querySelector('.edit-name').value.trim(); const newAdresse = li.querySelector('.edit-adresse').value.trim(); if (!newName || !newAdresse) { alert('Name und Adresse dürfen nicht leer sein.'); return; } chrome.storage.sync.get({ services: [] }, function(data) { const services = data.services || []; const idx = services.findIndex(s => s.name === service.name && (s.adresse || '') === (service.adresse || '')); if (idx === -1) { alert('Fehler: Dienst nicht gefunden.'); renderServices(searchInput.value.trim()); return; } services[idx].name = newName; services[idx].adresse = newAdresse; chrome.storage.sync.set({ services }, () => { renderServices(searchInput.value.trim()); renderPopupServerSelection(services); // Auch die Auswahl neu rendern }); }); }); } // --- Add service --- function addService(name, adresse) { if (!name || !adresse) return; chrome.storage.sync.get({ services: [] }, function(data) { const newService = { name, adresse }; const updatedServices = [...data.services, newService]; chrome.storage.sync.set({ services: updatedServices }, () => { renderServices(searchInput.value.trim()); renderPopupServerSelection(updatedServices); // Auch die Auswahl neu rendern }); }); } serviceForm.addEventListener('submit', function(event) { event.preventDefault(); const nameInput = document.getElementById('service-name'); const protocolSelect = document.getElementById('service-protocol'); const domainInput = document.getElementById('service-domain'); const fullAdresse = protocolSelect.value + domainInput.value.trim(); addService(nameInput.value.trim(), fullAdresse); serviceForm.reset(); nameInput.focus(); }); // --- Settings load/save --- function loadSettings() { // NEU: discordWebhookUrl, telegramBotToken und telegramChatId zu den abgerufenen Daten hinzufügen chrome.storage.sync.get({ checkInterval: 1, notifyOnline: false, discordWebhookUrl: '', telegramBotToken: '', telegramChatId: '' }, function(data) { intervalSelect.value = data.checkInterval; notifyOnlineCheckbox.checked = data.notifyOnline; discordWebhookInput.value = data.discordWebhookUrl || ''; telegramBotTokenInput.value = data.telegramBotToken || ''; telegramChatIdInput.value = data.telegramChatId || ''; }); } intervalSelect.addEventListener('change', () => { const newInterval = parseFloat(intervalSelect.value); chrome.storage.sync.set({ checkInterval: newInterval }, () => { chrome.runtime.sendMessage({ type: 'updateInterval' }); }); }); notifyOnlineCheckbox.addEventListener('change', () => { chrome.storage.sync.set({ notifyOnline: notifyOnlineCheckbox.checked }); }); // NEU: Event Listener für Discord Webhook URL discordWebhookInput.addEventListener('change', () => { chrome.storage.sync.set({ discordWebhookUrl: discordWebhookInput.value.trim() }); }); // NEU: Event Listener für Telegram telegramBotTokenInput.addEventListener('change', () => { chrome.storage.sync.set({ telegramBotToken: telegramBotTokenInput.value.trim() }); }); telegramChatIdInput.addEventListener('change', () => { chrome.storage.sync.set({ telegramChatId: telegramChatIdInput.value.trim() }); }); // --- ERWEITERTES Import/Export --- exportBtn.addEventListener('click', () => { // Rufe alle relevanten Daten aus dem Speicher ab, inklusive der neuen Telegram-Einstellungen chrome.storage.sync.get({ services: [], popupServers: [], checkInterval: 1, notifyOnline: false, discordWebhookUrl: '', telegramBotToken: '', telegramChatId: '' }, (data) => { // Erstelle einen String aus den Daten für die JSON-Datei const dataStr = JSON.stringify(data, null, 2); const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'uptime-monitor-backup.json'; // Passenderer Dateiname document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); }); importBtn.addEventListener('click', () => importFileInput.click()); importFileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const importedData = JSON.parse(e.target.result); // Prüfe, ob die importierten Daten ein Objekt sind if (typeof importedData === 'object' && importedData !== null) { // Setze alle importierten Daten auf einmal chrome.storage.sync.set(importedData, () => { alert('Backup erfolgreich wiederhergestellt! Die Seite wird neu geladen, um alle Änderungen zu übernehmen.'); // Nach dem Import ist es am sichersten, die Seite neu zu laden location.reload(); }); } else { alert('Ungültiges Dateiformat. Die Datei muss eine gültige Backup-Datei sein.'); } } catch (error) { console.error('Fehler beim Verarbeiten der Import-Datei:', error); alert('Fehler beim Lesen der Datei. Bitte stellen Sie sicher, dass es sich um eine gültige JSON-Datei handelt.'); } }; reader.readAsText(file); }); // --- Statistik-Anzeige --- function showStatsForService(service) { if (!service) return; document.getElementById('no-service-selected').style.display = 'none'; const chartCanvas = document.getElementById('uptimeChart'); chartCanvas.style.display = 'block'; chrome.storage.local.get({ history: {} }, (data) => { const history = data.history[service.name] || {}; const labels = Object.keys(history).sort().slice(-48); const uptimeData = labels.map(label => { const h = history[label]; return h.checks > 0 ? (h.up_checks / h.checks * 100).toFixed(2) : 0; }); if (currentChart) currentChart.destroy(); const ctx = chartCanvas.getContext('2d'); if (labels.length === 0) { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.font = "16px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto"; ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-secondary'); ctx.textAlign = 'center'; ctx.fillText('Noch keine Daten verfügbar.', ctx.canvas.width / 2, ctx.canvas.height / 2); return; } const formattedLabels = labels.map(l => { const parts = l.split(' '); return parts.length === 2 ? parts[1] : l; }); currentChart = new Chart(ctx, { type: 'line', data: { labels: formattedLabels, datasets: [{ label: `Uptime für ${service.name} (%)`, data: uptimeData, borderColor: 'var(--accent-color)', backgroundColor: 'rgba(24, 119, 242, 0.1)', fill: true, tension: 0.3 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: 100, ticks: { callback: value => value + '%' } } }, plugins: { legend: { display: false } } } }); }); } // --- Search --- searchInput.addEventListener('input', () => { renderServices(searchInput.value.trim()); }); // --- Helpers --- function escapeHtml(str) { if (!str) return ''; return String(str).replace(/[&<>"']/g, s => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[s])); } function escapeAttr(s) { return (s||'').replace(/"/g, '"'); } // --- Init --- loadSettings(); renderServices(); // Initiales Rendern der Popup-Auswahl chrome.storage.sync.get({ services: [] }, (data) => { renderPopupServerSelection(data.services || []); }); // --- ROBUSTE: Gitea Repository Widget --- // Führt mehrere Suchen durch und kombiniert die Ergebnisse. function fetchAndDisplayGiteaRepos() { const list = document.getElementById('gitea-repos-list'); const loadingIndicator = document.getElementById('gitea-repos-loading'); const errorIndicator = document.getElementById('gitea-repos-error'); const searchKeywords = ["Gitea", "Themes", "Server", "Minecraft", "Webseite", "Wordpress", "Plugin", "chrome-erweiterung"]; loadingIndicator.style.display = 'block'; list.style.display = 'none'; errorIndicator.style.display = 'none'; // Erstelle eine Liste von Promises, eine für jede Suchanfrage const searchPromises = searchKeywords.map(keyword => { const apiUrl = `https://git.viper.ipv64.net/api/v1/repos/search?q=${encodeURIComponent(keyword)}&limit=50&sort=updated&order=desc`; return fetch(apiUrl).then(response => { if (!response.ok) { // Wenn eine Anfrage fehlschlägt, geben wir ein leeres Array zurück, um die anderen nicht zu blockieren console.warn(`Suche nach "${keyword}" fehlgeschlagen:`, response.status); return { data: [] }; // Gib eine leere Antwort-Struktur zurück } return response.json(); }); }); // Führe alle Suchanfragen parallel aus Promise.all(searchPromises) .then(results => { // Kombiniere alle Ergebnisse aus den einzelnen Suchen const allRepos = results.flatMap(result => result.data || []); // Entferne Duplikate anhand der einzigartigen ID des Repositories const uniqueReposMap = new Map(); allRepos.forEach(repo => { uniqueReposMap.set(repo.id, repo); }); const uniqueRepos = Array.from(uniqueReposMap.values()); // Sortiere die kombinierten Repositories nach dem letzten Update-Datum uniqueRepos.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); // Begrenze die Ergebnisse auf die 5 neuesten const finalRepos = uniqueRepos.slice(0, 5); loadingIndicator.style.display = 'none'; list.innerHTML = ''; if (finalRepos.length > 0) { list.style.display = 'block'; finalRepos.forEach(repo => { const li = document.createElement('li'); const link = document.createElement('a'); link.href = repo.html_url; link.textContent = repo.name; link.target = '_blank'; link.rel = 'noopener noreferrer'; li.appendChild(link); list.appendChild(li); }); } else { list.style.display = 'block'; list.innerHTML = '
  • Keine passenden Repositories gefunden.
  • '; } }) .catch(error => { console.error('Fehler beim Abrufen der Gitea Repositories:', error); loadingIndicator.style.display = 'none'; errorIndicator.style.display = 'block'; }); } // Rufe die neue Funktion auf, wenn die Seite geladen wird. fetchAndDisplayGiteaRepos(); });