Handle browser-blocked status checks
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s

This commit is contained in:
Kroonk
2026-05-21 15:40:50 +02:00
parent 2125601e56
commit 8f2b4faad9
8 changed files with 50 additions and 22 deletions

View File

@@ -140,4 +140,6 @@ mischlabs/
- Erlaubter Admin in der statischen UI: - Erlaubter Admin in der statischen UI:
- `preferred_username=mrdiderot` oder `email=mail.misch@pm.me` - `preferred_username=mrdiderot` oder `email=mail.misch@pm.me`
- Aktuelle Grenze: Weil mischlabs noch rein statisch ueber Nginx laeuft, kann `/admin.html` globale Aenderungen noch nicht serverseitig speichern. Der Editor speichert lokal im Browser (`localStorage`) und kann eine neue `services.json` exportieren. Fuer echte Live-Aenderungen braucht der naechste Schritt ein kleines Backend oder einen Gitea-Commit-Workflow. - Aktuelle Grenze: Weil mischlabs noch rein statisch ueber Nginx laeuft, kann `/admin.html` globale Aenderungen noch nicht serverseitig speichern. Der Editor speichert lokal im Browser (`localStorage`) und kann eine neue `services.json` exportieren. Fuer echte Live-Aenderungen braucht der naechste Schritt ein kleines Backend oder einen Gitea-Commit-Workflow.
- Statusdiagnose nutzt Browser-Fetch mit `mode: "no-cors"`. Dadurch erkennt sie Erreichbarkeit/Timeouts, aber bei fremden Subdomains keine echten HTTP-Statuscodes. Fuer echte Fehlerdetails braucht es ebenfalls ein Backend. - Statusdiagnose nutzt Browser-Fetch mit `mode: "no-cors"`. Dadurch erkennt sie Erreichbarkeit/Timeouts, aber bei fremden Subdomains keine echten HTTP-Statuscodes.
- Wenn der Browser die Pruefung wegen CORS/CORB blockiert (`Failed to fetch`), wird der Dienst **nicht** mehr als offline markiert, sondern als "Nicht im Browser pruefbar". Das betraf z. B. Vaultwarden/`password.mischlabs.de`.
- Fuer echte Fehlerdetails braucht es ein kleines Backend bzw. einen Statusproxy, der serverseitig `HEAD`/`GET` prueft.

View File

@@ -6,7 +6,7 @@
<title>MischLabs Admin</title> <title>MischLabs Admin</title>
<meta name="description" content="MischLabs dashboard administration."> <meta name="description" content="MischLabs dashboard administration.">
<meta name="theme-color" content="#0f172a"> <meta name="theme-color" content="#0f172a">
<link rel="stylesheet" href="/style.css?v=10"> <link rel="stylesheet" href="/style.css?v=11">
</head> </head>
<body> <body>
<header class="shell hero admin-hero"> <header class="shell hero admin-hero">
@@ -57,6 +57,6 @@
</section> </section>
</main> </main>
<script src="/admin.js?v=1" defer></script> <script src="/admin.js?v=2" defer></script>
</body> </body>
</html> </html>

View File

@@ -217,17 +217,20 @@ async function probe(url) {
signal: controller.signal signal: controller.signal
}); });
return { return {
ok: true, status: 'online',
ms: Math.round(performance.now() - started), ms: Math.round(performance.now() - started),
detail: response.type === 'opaque' detail: response.type === 'opaque'
? 'Erreichbar. HTTP-Details wegen Browser-CORS nicht lesbar.' ? 'Erreichbar. HTTP-Details wegen Browser-CORS nicht lesbar.'
: `HTTP ${response.status}` : `HTTP ${response.status}`
}; };
} catch (error) { } catch (error) {
const isTimeout = error.name === 'AbortError';
return { return {
ok: false, status: isTimeout ? 'offline' : 'unknown',
ms: Math.round(performance.now() - started), ms: Math.round(performance.now() - started),
detail: error.name === 'AbortError' ? 'Timeout nach 6 Sekunden.' : error.message detail: isTimeout
? 'Timeout nach 6 Sekunden.'
: 'Der Browser blockiert diese Pruefung wahrscheinlich wegen CORS/CORB. Der Dienst kann trotzdem online sein.'
}; };
} finally { } finally {
window.clearTimeout(timeout); window.clearTimeout(timeout);
@@ -238,12 +241,20 @@ async function checkRow(row) {
const service = config.services[Number(row.dataset.index)]; const service = config.services[Number(row.dataset.index)];
const diagnostic = row.querySelector('.diagnostic'); const diagnostic = row.querySelector('.diagnostic');
diagnostic.textContent = 'Pruefe...'; diagnostic.textContent = 'Pruefe...';
row.classList.remove('is-online', 'is-offline'); row.classList.remove('is-online', 'is-offline', 'is-unknown');
const result = await probe(service.url); const result = await probe(service.url);
row.classList.toggle('is-online', result.ok); row.classList.toggle('is-online', result.status === 'online');
row.classList.toggle('is-offline', !result.ok); row.classList.toggle('is-offline', result.status === 'offline');
diagnostic.textContent = `${result.ok ? 'Online' : 'Fehler'} - ${result.ms} ms - ${result.detail}`; row.classList.toggle('is-unknown', result.status === 'unknown');
const label = {
online: 'Online',
offline: 'Fehler',
unknown: 'Nicht im Browser pruefbar'
}[result.status];
diagnostic.textContent = `${label} - ${result.ms} ms - ${result.detail}`;
} }
function saveLocal() { function saveLocal() {
@@ -315,4 +326,3 @@ downloadConfigButton.addEventListener('click', downloadConfig);
boot().catch((error) => { boot().catch((error) => {
loginMessage.textContent = `Adminseite konnte nicht starten: ${error.message}`; loginMessage.textContent = `Adminseite konnte nicht starten: ${error.message}`;
}); });

17
app.js
View File

@@ -154,13 +154,19 @@ async function probeService(card) {
}); });
card.classList.add('is-online'); card.classList.add('is-online');
card.classList.remove('is-offline'); card.classList.remove('is-offline');
card.classList.remove('is-unknown');
card.querySelector('.status-dot').setAttribute('aria-label', 'Online'); card.querySelector('.status-dot').setAttribute('aria-label', 'Online');
return true; return true;
} catch { } catch (error) {
card.classList.add('is-offline'); const isTimeout = error.name === 'AbortError';
card.classList.toggle('is-offline', isTimeout);
card.classList.toggle('is-unknown', !isTimeout);
card.classList.remove('is-online'); card.classList.remove('is-online');
card.querySelector('.status-dot').setAttribute('aria-label', 'Nicht erreichbar'); card.querySelector('.status-dot').setAttribute(
return false; 'aria-label',
isTimeout ? 'Nicht erreichbar' : 'Im Browser nicht pruefbar'
);
return isTimeout ? false : null;
} finally { } finally {
window.clearTimeout(timeout); window.clearTimeout(timeout);
} }
@@ -169,11 +175,12 @@ async function probeService(card) {
async function refreshStatus() { async function refreshStatus() {
const results = await Promise.allSettled(cards.map(probeService)); const results = await Promise.allSettled(cards.map(probeService));
const online = results.filter((result) => result.status === 'fulfilled' && result.value).length; const online = results.filter((result) => result.status === 'fulfilled' && result.value).length;
const unknown = results.filter((result) => result.status === 'fulfilled' && result.value === null).length;
onlineCount.textContent = String(online); onlineCount.textContent = String(online);
lastChecked.textContent = `Status geprueft: ${new Date().toLocaleTimeString('de-DE', { lastChecked.textContent = `Status geprueft: ${new Date().toLocaleTimeString('de-DE', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
})}`; })}${unknown ? `, ${unknown} im Browser nicht pruefbar` : ''}`;
} }
function scheduleStatusRefresh() { function scheduleStatusRefresh() {

View File

@@ -13,7 +13,7 @@
<link rel="apple-touch-icon" href="/icons/apple-touch-icon-v2.png"> <link rel="apple-touch-icon" href="/icons/apple-touch-icon-v2.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png"> <link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512.png"> <link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512.png">
<link rel="stylesheet" href="style.css?v=10"> <link rel="stylesheet" href="style.css?v=11">
</head> </head>
<body> <body>
<header class="shell hero"> <header class="shell hero">
@@ -73,6 +73,6 @@
<span id="lastChecked">Status wird im Hintergrund geprueft.</span> <span id="lastChecked">Status wird im Hintergrund geprueft.</span>
</footer> </footer>
<script src="/app.js?v=2" defer></script> <script src="/app.js?v=3" defer></script>
</body> </body>
</html> </html>

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MischLabs - Offline</title> <title>MischLabs - Offline</title>
<meta name="theme-color" content="#0f172a"> <meta name="theme-color" content="#0f172a">
<link rel="stylesheet" href="/style.css?v=10"> <link rel="stylesheet" href="/style.css?v=11">
</head> </head>
<body> <body>
<main class="shell hero"> <main class="shell hero">

View File

@@ -387,6 +387,11 @@ main {
box-shadow: 0 0 0 4px rgba(251, 113, 133, 0.1); box-shadow: 0 0 0 4px rgba(251, 113, 133, 0.1);
} }
.service-card.is-unknown .status-dot {
background: #38bdf8;
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.1);
}
.empty-state { .empty-state {
margin: 32px 0; margin: 32px 0;
padding: 18px; padding: 18px;
@@ -555,6 +560,10 @@ main {
border-color: rgba(251, 113, 133, 0.36); border-color: rgba(251, 113, 133, 0.36);
} }
.admin-row.is-unknown {
border-color: rgba(56, 189, 248, 0.32);
}
@media (max-width: 900px) { @media (max-width: 900px) {
.hero { .hero {
grid-template-columns: 1fr; grid-template-columns: 1fr;

6
sw.js
View File

@@ -1,10 +1,10 @@
const CACHE_NAME = 'mischlabs-pwa-v4'; const CACHE_NAME = 'mischlabs-pwa-v5';
const APP_SHELL = [ const APP_SHELL = [
'/', '/',
'/index.html', '/index.html',
'/offline.html', '/offline.html',
'/style.css?v=10', '/style.css?v=11',
'/app.js?v=2', '/app.js?v=3',
'/services.json?v=1', '/services.json?v=1',
'/manifest.webmanifest?v=3', '/manifest.webmanifest?v=3',
'/icons/icon-192.png', '/icons/icon-192.png',