feat: Add premium server-side Dienste-Status (Realtime) Board to admin panel
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s

This commit is contained in:
Kroonk
2026-05-21 18:04:53 +02:00
parent d4f6e56ff0
commit e603bdc3be
3 changed files with 266 additions and 2 deletions

112
admin.js
View File

@@ -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 = '<div class="ops-line">Keine Dienst-Statusdaten vom Server erhalten.</div>';
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 = `<span style="color: var(--down); font-weight: 700;">${offlineCount} offline</span> / ${totalCount} gesamt`;
} else {
summaryEl.innerHTML = `<span style="color: var(--ok); font-weight: 700;">Alle online</span> (${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 `
<div class="status-board-item ${statusClass}">
<div class="status-board-item-header">
<h4>${item.name}</h4>
<span class="status-board-item-latency">${latencyText}</span>
</div>
<div class="status-board-item-body">
<span class="status-board-item-domain">${item.domain}</span>
<div class="status-board-item-info">
<span class="status-board-item-status">${statusLabel}</span>
</div>
</div>
</div>
`;
}).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);