options.js aktualisiert
This commit is contained in:
881
options.js
881
options.js
@@ -1,382 +1,501 @@
|
||||
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 = `
|
||||
<input type="checkbox" id="popup-server-${service.name.replace(/\s+/g, '-')}" ${isSelected ? 'checked' : ''}>
|
||||
<div class="server-info">
|
||||
<div class="server-name">${escapeHtml(service.name)}</div>
|
||||
<div class="server-address">${escapeHtml(service.adresse || '')}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="service-left">
|
||||
<div class="service-name" role="button" tabindex="0">${escapeHtml(service.name)}</div>
|
||||
<div class="service-sub">${escapeHtml(service.adresse || '')}</div>
|
||||
</div>
|
||||
<div class="service-right">
|
||||
<div class="service-meta">
|
||||
<span class="status-dot small ${st}" title="Status: ${st}"></span>
|
||||
<span class="response-time">${resp}</span>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="icon-btn edit" aria-label="Bearbeiten ${escapeHtml(service.name)}" title="Bearbeiten">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9"></path>
|
||||
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn delete" aria-label="Löschen ${escapeHtml(service.name)}" title="Löschen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="edit-left">
|
||||
<input class="edit-name" value="${escapeAttr(service.name)}" />
|
||||
<input class="edit-adresse" value="${escapeAttr(service.adresse || '')}" />
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button class="btn-save" title="Speichern">Speichern</button>
|
||||
<button class="btn-cancel" title="Abbrechen">Abbrechen</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 || []);
|
||||
});
|
||||
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 = `
|
||||
<input type="checkbox" id="popup-server-${service.name.replace(/\s+/g, '-')}" ${isSelected ? 'checked' : ''}>
|
||||
<div class="server-info">
|
||||
<div class="server-name">${escapeHtml(service.name)}</div>
|
||||
<div class="server-address">${escapeHtml(service.adresse || '')}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="service-left">
|
||||
<div class="service-name" role="button" tabindex="0">${escapeHtml(service.name)}</div>
|
||||
<div class="service-sub">${escapeHtml(service.adresse || '')}</div>
|
||||
</div>
|
||||
<div class="service-right">
|
||||
<div class="service-meta">
|
||||
<span class="status-dot small ${st}" title="Status: ${st}"></span>
|
||||
<span class="response-time">${resp}</span>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="icon-btn edit" aria-label="Bearbeiten ${escapeHtml(service.name)}" title="Bearbeiten">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9"></path>
|
||||
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn delete" aria-label="Löschen ${escapeHtml(service.name)}" title="Löschen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="edit-left">
|
||||
<input class="edit-name" value="${escapeAttr(service.name)}" />
|
||||
<input class="edit-adresse" value="${escapeAttr(service.adresse || '')}" />
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button class="btn-save" title="Speichern">Speichern</button>
|
||||
<button class="btn-cancel" title="Abbrechen">Abbrechen</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<li>Keine passenden Repositories gefunden.</li>';
|
||||
}
|
||||
})
|
||||
.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();
|
||||
});
|
||||
Reference in New Issue
Block a user