diff --git a/admin.html b/admin.html index 326ad74..dd912cf 100644 --- a/admin.html +++ b/admin.html @@ -6,7 +6,7 @@ MischLabs Admin - +
@@ -54,6 +54,17 @@
+
+
+
+ Dienste-Status (Live-Proxy) + +
+ +
+
Lade Status...
+
+
NAS Speicher @@ -83,6 +94,6 @@
- + diff --git a/admin.js b/admin.js index b275c65..e7e821f 100644 --- a/admin.js +++ b/admin.js @@ -25,6 +25,8 @@ const dangerState = document.querySelector('#dangerState'); const diskStatus = document.querySelector('#diskStatus'); const watchtowerStatus = document.querySelector('#watchtowerStatus'); const containerStatus = document.querySelector('#containerStatus'); +const servicesStatusBoard = document.querySelector('#servicesStatusBoard'); +const refreshServicesButton = document.querySelector('#refreshServicesButton'); let config = null; let tokenSet = null; @@ -298,6 +300,106 @@ function formatBytes(bytes) { return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`; } +function renderServicesStatus(services = []) { + if (!servicesStatusBoard) return; + if (!Array.isArray(services) || services.length === 0) { + servicesStatusBoard.innerHTML = '
Keine Dienst-Statusdaten vom Server erhalten.
'; + return; + } + + // Create a map of status by URL for easy lookup + const statusMap = new Map(); + services.forEach((s) => { + if (s && s.url) { + statusMap.set(s.url, s); + } + }); + + // We want to render: + // 1. The main Landing Page (mischlabs.de) + // 2. All services defined in the config (from config.services) + const itemsToRender = []; + + // Main landing page check + const mainSiteUrl = 'https://mischlabs.de'; + const mainSiteStatus = statusMap.get(mainSiteUrl); + if (mainSiteStatus) { + itemsToRender.push({ + name: 'MischLabs Landing Page', + domain: 'mischlabs.de', + url: mainSiteUrl, + status: mainSiteStatus + }); + } + + // Services from editor config + if (config && Array.isArray(config.services)) { + config.services.forEach((service) => { + const serviceStatus = statusMap.get(service.url); + if (serviceStatus) { + itemsToRender.push({ + name: service.name, + domain: service.domain, + url: service.url, + status: serviceStatus + }); + } else { + // Fallback if URL didn't match exactly but we can find a substring/partial match, + // or just show it as unknown + const fallbackStatus = services.find(s => s && s.url && s.url.includes(service.domain)); + itemsToRender.push({ + name: service.name, + domain: service.domain, + url: service.url, + status: fallbackStatus || { url: service.url, ok: false, error: 'Nicht geprueft' } + }); + } + }); + } + + const offlineCount = itemsToRender.filter(item => !item.status.ok).length; + const totalCount = itemsToRender.length; + const summaryEl = document.querySelector('#servicesStatusSummary'); + if (summaryEl) { + if (offlineCount > 0) { + summaryEl.innerHTML = `${offlineCount} offline / ${totalCount} gesamt`; + } else { + summaryEl.innerHTML = `Alle online (${totalCount})`; + } + } + + servicesStatusBoard.innerHTML = itemsToRender.map((item) => { + const s = item.status; + let statusClass = 'is-online'; + let statusLabel = `HTTP ${s.statusCode || '200'}`; + let latencyText = `${s.ms ?? '--'} ms`; + + if (s.error || s.ok === false) { + statusClass = 'is-offline'; + statusLabel = s.statusCode ? `HTTP ${s.statusCode}` : 'Offline'; + latencyText = s.error || 'Verbindung fehlgeschlagen'; + } else if (s.ms > 1000) { + statusClass = 'is-warning'; + statusLabel = 'Traege'; + } + + return ` +
+
+

${item.name}

+ ${latencyText} +
+
+ ${item.domain} +
+ ${statusLabel} +
+
+
+ `; + }).join(''); +} + function renderDisks(disks = []) { diskStatus.innerHTML = disks.map((disk) => { if (disk.error) { @@ -565,6 +667,9 @@ async function refreshOps(options = {}) { diskStatus.textContent = 'Lade...'; watchtowerStatus.textContent = 'Lade...'; containerStatus.textContent = 'Lade...'; + if (servicesStatusBoard) { + servicesStatusBoard.textContent = 'Lade...'; + } } try { @@ -574,10 +679,14 @@ async function refreshOps(options = {}) { renderDisks(status.disks); renderWatchtower(status.watchtower); renderContainers(status.containers); + renderServicesStatus(status.services); } catch (error) { diskStatus.textContent = `Statusproxy nicht erreichbar: ${error.message}`; watchtowerStatus.textContent = 'Nicht verfuegbar.'; containerStatus.textContent = 'Nicht verfuegbar.'; + if (servicesStatusBoard) { + servicesStatusBoard.textContent = `Nicht verfuegbar: ${error.message}`; + } } } @@ -644,6 +753,9 @@ resetLocalButton.addEventListener('click', () => { downloadConfigButton.addEventListener('click', downloadConfig); refreshOpsButton.addEventListener('click', refreshOps); +if (refreshServicesButton) { + refreshServicesButton.addEventListener('click', () => refreshOps()); +} runWatchtowerButton.addEventListener('click', runWatchtowerOnce); dangerModeButton.addEventListener('click', unlockDangerMode); diff --git a/style.css b/style.css index 0d541e2..26ce6eb 100644 --- a/style.css +++ b/style.css @@ -909,3 +909,144 @@ main { bottom: -10px; } } + +/* ========================================================================== + Dienste Status Board (Live-Proxy) + ========================================================================== */ +.services-status-board { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 12px; + margin-top: 6px; +} + +.status-board-item { + display: flex; + flex-direction: column; + padding: 13px 14px; + background: rgba(14, 19, 31, 0.45); + border: 1px solid var(--border); + border-radius: var(--radius); + transition: all 0.22s cubic-bezier(0.165, 0.84, 0.44, 1); + position: relative; + overflow: hidden; +} + +.status-board-item:hover { + background: rgba(23, 29, 45, 0.65); + border-color: var(--border-strong); + transform: translateY(-2px); +} + +.status-board-item-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 5px; +} + +.status-board-item-header h4 { + margin: 0; + font-size: 0.92rem; + font-weight: 650; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-board-item-body { + display: flex; + flex-direction: column; + gap: 4px; +} + +.status-board-item-domain { + font-size: 0.76rem; + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-board-item-info { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 6px; + font-size: 0.78rem; +} + +.status-board-item-status { + font-weight: 650; + font-size: 0.78rem; +} + +.status-board-item-latency { + font-size: 0.74rem; + color: var(--dim); +} + +/* Status styles with glowing left bars */ +.status-board-item.is-online { + border-color: rgba(52, 211, 153, 0.16); + background: rgba(16, 185, 129, 0.02); +} +.status-board-item.is-online .status-board-item-status { + color: var(--ok); +} +.status-board-item.is-online::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + background: var(--ok); + box-shadow: 0 0 8px var(--ok); +} + +.status-board-item.is-offline { + border-color: rgba(239, 68, 68, 0.3); + background: rgba(239, 68, 68, 0.05); +} +.status-board-item.is-offline .status-board-item-status { + color: var(--down); +} +.status-board-item.is-offline::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + background: var(--down); + box-shadow: 0 0 8px var(--down); +} + +.status-board-item.is-warning { + border-color: rgba(245, 158, 11, 0.25); + background: rgba(245, 158, 11, 0.04); +} +.status-board-item.is-warning .status-board-item-status { + color: var(--warn); +} +.status-board-item.is-warning::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + background: var(--warn); + box-shadow: 0 0 8px var(--warn); +} + +@media (max-width: 620px) { + .services-status-board { + grid-template-columns: 1fr; + gap: 8px; + } +} +