diff --git a/background.js b/background.js new file mode 100644 index 0000000..ecd5c45 --- /dev/null +++ b/background.js @@ -0,0 +1,165 @@ +// In Minuten (1 Minute) +const CHECK_INTERVAL_MINUTES = 1; +const HISTORY_RETENTION_DAYS = 7; + +// Funktion, um einen einzelnen Dienst zu prüfen und die Antwortzeit zu messen +async function checkService(service) { + const startTime = performance.now(); + try { + const response = await fetch(service.adresse, { method: 'HEAD', mode: 'no-cors', cache: 'no-cache' }); + const endTime = performance.now(); + const responseTime = Math.round(endTime - startTime); + return { status: 'online', responseTime }; + } catch (error) { + return { status: 'offline', responseTime: null }; + } +} + +// Funktion, um Verlaufsdaten zu aktualisieren +async function updateHistory(serviceName, status) { + const data = await chrome.storage.local.get({ history: {} }); + const history = data.history; + const now = new Date(); + const hourKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}`; + + if (!history[serviceName]) history[serviceName] = {}; + if (!history[serviceName][hourKey]) history[serviceName][hourKey] = { checks: 0, up_checks: 0 }; + + history[serviceName][hourKey].checks++; + if (status === 'online') history[serviceName][hourKey].up_checks++; + + await chrome.storage.local.set({ history }); +} + +// Funktion, um alte Verlaufsdaten zu löschen +async function cleanupHistory() { + const data = await chrome.storage.local.get({ history: {} }); + const history = data.history; + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - HISTORY_RETENTION_DAYS); + const cutoffHourKey = `${cutoffDate.getFullYear()}-${String(cutoffDate.getMonth() + 1).padStart(2, '0')}-${String(cutoffDate.getDate()).padStart(2, '0')}-${String(cutoffDate.getHours()).padStart(2, '0')}`; + + for (const serviceName in history) { + for (const hourKey in history[serviceName]) { + if (hourKey < cutoffHourKey) delete history[serviceName][hourKey]; + } + if (Object.keys(history[serviceName]).length === 0) delete history[serviceName]; + } + await chrome.storage.local.set({ history }); +} + +// NEU: Funktion zum Senden einer Discord-Benachrichtigung +async function sendDiscordNotification(service, status, responseTime = null) { + const settings = await chrome.storage.sync.get({ discordWebhookUrl: '' }); + if (!settings.discordWebhookUrl) return; // Kein Webhook konfiguriert + + const isOnline = status === 'online'; + const embed = { + title: `Status-Update: ${service.name}`, + url: service.adresse, + description: isOnline ? '✅ Der Server ist wieder online.' : '❌ Der Server ist nicht erreichbar.', + color: isOnline ? 0x31A24C : 0xE4606D, // Grün / Rot + timestamp: new Date().toISOString(), + fields: [ + { name: 'Status', value: status.toUpperCase(), inline: true }, + { name: 'Adresse', value: service.adresse, inline: true } + ] + }; + + if (isOnline && responseTime !== null) { + embed.fields.push({ name: 'Antwortzeit', value: `${responseTime} ms`, inline: true }); + } + + const payload = { + username: 'Uptime Monitor', + embeds: [embed] + }; + + try { + const response = await fetch(settings.discordWebhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (!response.ok) { + console.error('Discord-Webhook fehlgeschlagen:', response.statusText, await response.text()); + } + } catch (error) { + console.error('Fehler beim Senden der Discord-Benachrichtigung:', error); + } +} + +// MODIFIZIERT: Funktion, um alle Dienste zu prüfen und Benachrichtigungen zu senden +async function checkAllServices() { + const data = await chrome.storage.sync.get({ services: [], notifyOnline: false }); + const services = data.services; + const notifyOnline = data.notifyOnline; + + if (services.length === 0) return; + + const lastStatusData = await chrome.storage.local.get({ serviceStatus: {} }); + const lastStatus = lastStatusData.serviceStatus; + + for (const service of services) { + const currentResult = await checkService(service); + const previousResult = lastStatus[service.name]; + lastStatus[service.name] = currentResult; + + // Benachrichtigung für Offline-Wechsel + if (previousResult?.status === 'online' && currentResult.status === 'offline') { + chrome.notifications.create({ + type: 'basic', iconUrl: 'icons/notification_warning.png', + title: `Server "${service.name}" ist offline`, + message: 'Der Dienst antwortet nicht mehr.', contextMessage: `Adresse: ${service.adresse}`, + priority: 1, isClickable: true, + buttons: [{ title: 'Anzeigen' }, { title: 'Ignorieren' }] + }); + await sendDiscordNotification(service, 'offline'); // NEU + } + + // Benachrichtigung für Online-Wechsel (wenn aktiviert) + if (notifyOnline && previousResult?.status === 'offline' && currentResult.status === 'online') { + chrome.notifications.create({ + type: 'basic', iconUrl: 'icons/notification_warning.png', + title: `Server "${service.name}" ist wieder online!`, + message: 'Der Dienst ist wieder erreichbar.', contextMessage: `Adresse: ${service.adresse}`, + priority: 0, isClickable: true, + buttons: [{ title: 'Anzeigen' }] + }); + await sendDiscordNotification(service, 'online', currentResult.responseTime); // NEU + } + + // Verlaufsdaten aktualisieren + await updateHistory(service.name, currentResult.status); + } + + await chrome.storage.local.set({ serviceStatus: lastStatus }); +} + +// Alarm und Setup-Logik +async function setupAlarm() { + const data = await chrome.storage.sync.get({ checkInterval: 1 }); + const intervalMinutes = data.checkInterval; + await chrome.alarms.clear('uptimeCheck'); + chrome.alarms.create('uptimeCheck', { periodInMinutes: intervalMinutes }); +} + +chrome.runtime.onInstalled.addListener(() => { setupAlarm(); }); +chrome.runtime.onStartup.addListener(() => { setupAlarm(); }); +chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'uptimeCheck') checkAllServices(); }); + +// Nachrichtensystem für Einstellungen +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'updateInterval') { setupAlarm().then(() => sendResponse({ status: 'ok' })); return true; } + if (message.type === 'cleanupHistory') { cleanupHistory().then(() => sendResponse({ status: 'ok' })); return true; } +}); + +// Benachrichtigungs-Listener +chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => { + if (buttonIndex === 0) chrome.action.openPopup(); + chrome.notifications.clear(notificationId); +}); +chrome.notifications.onClicked.addListener((notificationId) => { + chrome.action.openPopup(); + chrome.notifications.clear(notificationId); +}); \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..b6ce4e4 --- /dev/null +++ b/manifest.json @@ -0,0 +1,31 @@ +{ + "manifest_version": 3, + "name": "Uptime Monitor", + "version": "1.0", + "description": "Überwacht Dienste und benachrichtigt mich, wenn sie offline gehen.", + "permissions": [ + "storage", + "notifications", + "alarms" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "background.js" + }, + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.PNG", + "48": "icons/icon48.PNG", + "128": "icons/icon128.PNG" + } + }, + "options_page": "options.html", + "icons": { + "16": "icons/icon16.PNG", + "48": "icons/icon48.PNG", + "128": "icons/icon128.PNG" + } +} \ No newline at end of file diff --git a/options.html b/options.html new file mode 100644 index 0000000..8f111c8 --- /dev/null +++ b/options.html @@ -0,0 +1,145 @@ + + + + + Einstellungen - Uptime Monitor + + + + +
+
+ +
+ + +
+ + +
+
+ + + +
+
+

Neuen Dienst hinzufügen

+
+
+
+ +
+ + +
+
+ +
+
+ + +
+

Server für Popup auswählen (max. 8)

+

Wenn Sie mehr als 8 Server haben, wählen Sie hier aus, welche im Popup angezeigt werden sollen. Ansonsten werden alle Server angezeigt.

+ +
+ +
+

Überwachungseinstellungen

+
+ + +
+
+ + +
+
+ + +
+

Discord-Benachrichtigungen

+

Senden Sie Status-Updates an einen Discord-Kanal über einen Webhook. Dies ist optional.

+
+ + + Erstelle einen Webhook in deinen Discord-Servereinstellungen (Kanal > Integrationen > Webhooks) und füge die vollständige URL hier ein. Lasse das Feld leer, um die Funktion zu deaktivieren. +
+
+ +
+

Daten-Management

+
+ + + +
+
+
+ + +
+
+ + +
+
+ +
+
+

Serverliste

+ +
+

Klicke auf einen Dienstnamen, um seine Statistik rechts anzuzeigen. Benutze die Icons zum Bearbeiten / Löschen.

+
    + +
    + + +
    +

    Uptime-Statistik

    +
    +

    Wähle einen Dienst aus der Liste aus, um seine Statistik zu sehen.

    + +
    +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/options.js b/options.js new file mode 100644 index 0000000..ab32cdb --- /dev/null +++ b/options.js @@ -0,0 +1,382 @@ +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'); + + // --- 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.`); + checkbox.checked = false; // Haken entfernen + 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 zu den abgerufenen Daten hinzufügen + chrome.storage.sync.get({ checkInterval: 1, notifyOnline: false, discordWebhookUrl: '' }, function(data) { + intervalSelect.value = data.checkInterval; + notifyOnlineCheckbox.checked = data.notifyOnline; + discordWebhookInput.value = data.discordWebhookUrl || ''; // NEU + }); + } + 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() }); + }); + + // --- Import/Export --- + exportBtn.addEventListener('click', () => { + chrome.storage.sync.get({ services: [] }, (data) => { + const dataStr = JSON.stringify(data.services, 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-services.json'; + 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 importedServices = JSON.parse(e.target.result); + if (Array.isArray(importedServices)) { + chrome.storage.sync.set({ services: importedServices }, () => { + renderServices(searchInput.value.trim()); + renderPopupServerSelection(importedServices); // Auch die Auswahl neu rendern + }); + } + else { alert('Ungültiges Dateiformat.'); } + } catch (error) { alert('Fehler beim Lesen der Datei.'); } + }; + 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 || []); + }); +}); \ No newline at end of file diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..8caec10 --- /dev/null +++ b/popup.html @@ -0,0 +1,36 @@ + + + + + Uptime Monitor + + + + + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..24edc28 --- /dev/null +++ b/popup.js @@ -0,0 +1,100 @@ +document.addEventListener('DOMContentLoaded', () => { + const list = document.getElementById('service-list'); + const empty = document.getElementById('empty-state'); + const dot = document.getElementById('status-dot'); + const settingsBtn = document.getElementById('settings-btn'); + + const icons = { + online: ``, + offline: ``, + unknown: `` + }; + + function render() { + // Lade alle Server und die spezielle Auswahl für das Popup + chrome.storage.sync.get({services: [], popupServers: []}, (data) => { + chrome.storage.local.get({serviceStatus: {}}, (local) => { + const allServices = data.services; + const popupSelection = data.popupServers; + const statuses = local.serviceStatus; + + list.innerHTML = ''; + list.appendChild(empty); + + if (allServices.length === 0) { + dot.className = 'status-dot red'; + empty.style.display = 'flex'; + list.classList.remove('few-servers'); // Klasse entfernen + return; + } + + let displayServers = []; + + // Logik zur Bestimmung der anzuzeigenden Server + if (allServices.length <= 8) { + // Weniger als 8 Server: Alle anzeigen, Layout anpassen + displayServers = allServices; + list.classList.add('few-servers'); + } else { + // Mehr als 8 Server: Festes 2-Spalten-Layout + list.classList.remove('few-servers'); + if (popupSelection.length > 0) { + // Wenn eine Auswahl existiert, diese anzeigen + displayServers = popupSelection; + } else { + // Ansonsten die ersten 8 als Fallback + displayServers = allServices.slice(0, 8); + } + } + + const onlineCount = displayServers.filter(s => statuses[s.name]?.status === 'online').length; + + if (onlineCount === displayServers.length) dot.className = 'status-dot green pulse'; + else if (onlineCount === 0) dot.className = 'status-dot red'; + else dot.className = 'status-dot orange'; + + empty.style.display = 'none'; + + displayServers.forEach(s => { + const st = statuses[s.name] || {status: 'unknown', responseTime: null}; + + const card = document.createElement('div'); + card.className = 'service-card'; + + card.innerHTML = ` +
    +

    ${s.name}

    +

    ${s.adresse || ''}

    +
    +
    + ${ + st.status === 'online' + ? icons.online + 'Online' + (st.responseTime ? ` · ${st.responseTime} ms` : '') + : st.status === 'offline' + ? icons.offline + 'Offline' + : icons.unknown + 'Prüfung…' + } +
    + `; + + list.appendChild(card); + }); + }); + }); + } + + // Event Listener für Zahnrad-Button + if (settingsBtn) { + settingsBtn.addEventListener('click', () => { + chrome.runtime.openOptionsPage(() => { + if (chrome.runtime.lastError) { + console.error(chrome.runtime.lastError); + window.open('options.html', '_blank'); // Fallback + } + }); + }); + } + + render(); + chrome.storage.onChanged.addListener(render); +}); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..89fdaf0 --- /dev/null +++ b/style.css @@ -0,0 +1,429 @@ +/* ============================================= */ +/* UPTIME MONITOR – STYLE.CSS 2025 */ +/* Modern Popup – Kompakt & Clean */ +/* Optionspage bleibt unverändert */ +/* ============================================= */ + +/* === Farbvariablen === */ +:root { + --bg-color: #f0f2f5; + --card-bg: #ffffff; + --text-color: #1c1e21; + --text-secondary: #65676b; + --border-color: #dddfe2; + --shadow-color: rgba(0, 0, 0, 0.08); + --accent-color: #1877f2; + --online-color: #31a24c; + --offline-color: #e4606d; + --unknown-color: #8e8e93; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-color: #18191a; + --card-bg: #242526; + --text-color: #e4e6eb; + --text-secondary: #b0b3b8; + --border-color: #3e4042; + --shadow-color: rgba(0, 0, 0, 0.3); + --accent-color: #2d88ff; + } +} + +/* === Resets === */ +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background: var(--bg-color); + color: var(--text-color); + font-size: 15px; + line-height: 1.5; +} + +/* ==================== POPUP – Rahmen & Glas ==================== */ +.popup-body { margin: 0; padding: 0; background: transparent; } + +.popup { + width: 450px; + height: 540px; /* Feste Höhe hinzugefügt */ + display: flex; + flex-direction: column; /* Layout für Header und Content-Bereich */ + background: var(--bg-color); + border-radius: 20px; + overflow: hidden; + box-shadow: 0 20px 50px rgba(0,0,0,0.25); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + position: relative; + outline: 2px solid rgba(255,255,255,0.28); + outline-offset: -2px; +} + +.popup::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 20px; + padding: 1px; + background: linear-gradient(135deg, rgba(255,255,255,0.4), rgba(255,255,255,0.15) 40%, transparent); + -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: destination-out; + mask-composite: exclude; + pointer-events: none; + z-index: 10; +} + +.header { + flex-shrink: 0; /* Verhindert, dass der Header schrumpft */ + padding: 20px 24px 16px; + text-align: center; + background: linear-gradient(135deg, rgba(255,255,255,0.9), rgba(255,255,255,0.7)); + border-bottom: 1px solid var(--border-color); + z-index: 2; + position: relative; +} + +@media (prefers-color-scheme: dark) { + .header { background: linear-gradient(135deg, rgba(36,37,38,0.95), rgba(30,31,32,0.85)); } +} + +.header h1 { + margin: 0; + font-size: 19px; + font-weight: 700; + position: relative; +} + +/* Header Status Dot – nur im Header */ +.header .status-dot { + position: absolute; + top: 50%; + right: 22px; + width: 13px; + height: 13px; + border-radius: 50%; + background: var(--unknown-color); + transform: translateY(-50%); + z-index: 4; + pointer-events: none; + box-shadow: none; +} + +/* Zahnrad-Button im Header */ +.icon-btn#settings-btn { + position: absolute; + left: 20px; + top: 50%; + transform: translateY(-50%); + width: 28px; + height: 28px; + padding: 0; + border: none; + background: transparent; + cursor: pointer; + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + z-index: 3; +} + +.icon-btn#settings-btn svg { + width: 20px; + height: 20px; + pointer-events: none; +} + +.icon-btn#settings-btn:hover { + background: rgba(24,119,242,0.08); + color: var(--accent-color); + border-radius: 6px; +} + +/* ==================== STATUS DOTS für Server ==================== */ +.status-dot.small { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; + margin-right: 8px; +} + +.status-dot.small.online { background: var(--online-color); box-shadow: 0 0 6px rgba(49,162,76,0.25); } +.status-dot.small.offline { background: var(--offline-color); box-shadow: 0 0 6px rgba(228,96,109,0.25); } +.status-dot.small.unknown { background: var(--unknown-color); box-shadow: none; opacity:0.8; } + +.status-dot.green.pulse { background: #31a24c; animation: pulse-green 2s infinite; } +.status-dot.orange { background: #ff9f0a; animation: pulse-orange 2.5s infinite; } +.status-dot.red { background: #e4606d; animation: pulse-red 2s infinite; } + +@keyframes pulse-green { 0% { box-shadow: 0 0 0 0 rgba(49,162,76,0.7); } 70% { box-shadow: 0 0 0 12px rgba(49,162,76,0); } 100% { box-shadow: 0 0 0 0 rgba(49,162,76,0); } } +@keyframes pulse-orange { 0% { box-shadow: 0 0 0 0 rgba(255,159,10,0.7); } 70% { box-shadow: 0 0 0 12px rgba(255,159,10,0); } 100% { box-shadow: 0 0 0 0 rgba(255,159,10,0); } } +@keyframes pulse-red { 0% { box-shadow: 0 0 0 0 rgba(228,96,109,0.7); } 70% { box-shadow: 0 0 0 12px rgba(228,96,109,0); } 100% { box-shadow: 0 0 0 0 rgba(228,96,109,0); } } + +/* ==================== POPUP-SERVICE-LISTE – GRID ==================== */ +.services { + flex-grow: 1; /* Füllt den verfügbaren Platz im Flexbox-Container */ + padding: 14px 14px 18px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + overflow: hidden; /* Kein Scrollen */ +} + +/* Modifikator-Klasse für 8 oder weniger Server */ +.services.few-servers { + grid-template-columns: 1fr; /* Wechsel zu einer Spalte */ +} + +/* Kompakte kleine Kacheln */ +.service-card { + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px 14px; + background: var(--card-bg); + border-radius: 14px; + border: 1px solid var(--border-color); + transition: all 0.25s ease; +} + +.service-card:hover { + transform: translateY(-3px); + box-shadow: 0 10px 25px rgba(0,0,0,0.15); + border-color: transparent; +} + +/* Titel + Untertitel */ +.service-info h2 { margin: 0; font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.service-info p { margin: 0; font-size: 12px; opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +/* Status Badge */ +.status-badge { + padding: 4px 10px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + text-align: center; + align-self: flex-start; +} + +.status-badge.online { background: rgba(49,162,76,0.15); color: #31a24c; } +.status-badge.offline { background: rgba(228,96,109,0.15); color: #e4606d; } +.status-badge.unknown { background: rgba(142,142,147,0.15); color: #8e8e93; } + +/* Mini Response-Time */ +.service-card .response-time { font-size: 11px; opacity: 0.65; } + +/* ==================== EMPTY STATE ==================== */ +.empty-state { + text-align: center; + padding: 60px 20px; + color: var(--text-secondary); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.empty-icon { + width: 80px; + height: 80px; + margin-bottom: 20px; + background: var(--border-color); + border-radius: 20px; + opacity: 0.2; +} + +/* ==================== NEU: POPUP SERVER SELECTION ==================== */ +.popup-server-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 10px; + margin-top: 15px; + max-height: 300px; + overflow-y: auto; + padding-right: 5px; +} + +.popup-server-item { + display: flex; + align-items: center; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--card-bg); + transition: all 0.2s ease; +} + +.popup-server-item:hover { + background: rgba(24,119,242,0.05); + border-color: var(--accent-color); +} + +.popup-server-item input[type="checkbox"] { + margin-right: 10px; + width: 18px; + height: 18px; + accent-color: var(--accent-color); + cursor: pointer; +} + +.popup-server-item .server-info { + flex: 1; + cursor: pointer; +} + +.popup-server-item .server-name { + font-weight: 600; + margin-bottom: 3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.popup-server-item .server-address { + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.selection-info { + padding: 8px 12px; + background: rgba(24,119,242,0.1); + border-radius: 8px; + margin-bottom: 10px; + font-weight: 500; + color: var(--accent-color); +} + +/* ==================== FOOTER ==================== */ +.footer { + padding: 16px 24px 20px; + text-align: center; + background: var(--card-bg); + border-top: 1px solid var(--border-color); +} + +.footer a { + color: var(--accent-color); + font-weight: 600; + font-size: 15px; + padding: 10px 20px; + border-radius: 12px; +} + +.footer a:hover { background: rgba(24,119,242,0.1); } + +/* ==================== OPTIONS PAGE ==================== */ +.options-page { background: var(--bg-color); color: var(--text-color); padding: 20px; min-height: 100vh; } +.options-grid-container { max-width: 1400px; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; } + +.card, .widget-card { + background: var(--card-bg); + border-radius: 12px; + padding: 24px; + box-shadow: 0 4px 12px var(--shadow-color); + border: 1px solid var(--border-color); +} + +.card h1, .card h2, .widget-card h2 { + margin: 0 0 20px 0; + font-size: 20px; + font-weight: 600; + color: var(--text-color); +} + +.tab-nav { + display: flex; + background: var(--card-bg); + border-radius: 12px; + padding: 4px; + margin-bottom: 20px; + border: 1px solid var(--border-color); +} + +.tab-btn { flex: 1; padding: 12px 20px; background: none; color: var(--text-secondary); border: none; border-radius: 8px; cursor: pointer;} +.tab-btn.active { background: var(--accent-color); color: white; } + +.tab-panel { display: none; } +.tab-panel.active { display: block; } + +#manage-panel .manage-grid { display: grid; grid-template-columns: 1fr 3.8fr 1fr; gap: 28px; } +@media (max-width: 1100px) { #manage-panel .manage-grid { grid-template-columns: 1fr; } } + +.stats-grid { display: grid; grid-template-columns: 1fr 1.9fr; gap: 28px; } +@media (max-width: 900px) { .stats-grid { grid-template-columns: 1fr; } } + +.form-section { display: flex; flex-direction: column; gap: 20px; } +.form-group { margin-bottom: 16px; } + +label { display: block; margin-bottom: 6px; color: var(--text-secondary); } +input, select { width: 100%; padding: 12px 16px; border: 1px solid var(--border-color); border-radius: 8px; } + +.input-group { display: flex; gap: 8px; } +#service-protocol { width: 130px; } + +.btn { padding: 12px 20px; border-radius: 8px; font-size: 15px; cursor: pointer; border: none;} +.btn-primary { background: var(--accent-color); color: white; } +.btn-primary:hover { background: #166fe5; } +.btn-secondary { background: #eee; color: #333; } +.btn-secondary:hover { background: #ddd; } + +.checkbox-group { display: flex; align-items: center; gap: 12px; margin: 12px 0; } +.checkbox-group input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent-color); } + +.service-item { padding: 16px; border: 1px solid var(--border-color); border-radius: 8px; margin-bottom: 12px; } +.service-item:hover { box-shadow: 0 2px 8px var(--shadow-color); } + +.response-time { font-size: 12px; opacity: 0.7; } + +/* =========================== */ +/* Styles für die verbesserte Serverliste (Stats-Tab) */ +/* =========================== */ +.services-stat-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; } + +.service-stat-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 12px; + border-radius: 12px; + border: 1px solid var(--border-color); + background: var(--card-bg); + transition: box-shadow .12s, transform .12s; +} + +.service-stat-item:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,0.06); } +.service-stat-item.selected { outline: 2px solid rgba(24,119,242,0.12); box-shadow: 0 8px 22px rgba(24,119,242,0.06); } + +.service-left { display: flex; flex-direction: column; min-width: 0; gap: 4px; } +.service-name { font-weight: 600; font-size: 14px; color: var(--text-color); cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.service-sub { font-size: 12px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.service-right { display: flex; align-items: center; gap: 12px; } + +.service-meta { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--text-secondary); } + +/* icon buttons */ +.icon-btn { display: inline-flex; align-items: center; justify-content: center; padding:6px; border-radius:8px; border: none; background: transparent; cursor: pointer; transition: background .12s, color .12s; color: var(--text-secondary); } +.icon-btn:hover { background: rgba(24,119,242,0.06); color: var(--accent-color); } +.icon-btn.delete:hover { background: rgba(228,96,109,0.06); color: var(--offline-color); } + +/* edit inline */ +.service-stat-item.editing { padding: 8px; } +.edit-left { display:flex; flex-direction:column; gap:8px; width:100%; } +.edit-left input { padding:8px 10px; border-radius:8px; border:1px solid var(--border-color); background:var(--card-bg); } +.edit-actions { display:flex; gap:8px; align-items:center; } +.btn-save, .btn-cancel { padding:6px 10px; border-radius:8px; border:none; cursor:pointer; font-weight:600; } +.btn-save { background:var(--accent-color); color:white; } +.btn-cancel { background:#eee; color:#333; } + +/* Stats-Tab: Chart größer */ +.stats-chart-card { display: flex; flex-direction: column; padding: 16px; border-radius: 12px; background: var(--card-bg); flex: 2; min-height: 500px; } +#uptimeChart { width: 100% !important; height: 100% !important; display: block; } \ No newline at end of file