From 8f2b4faad9344c8a14f28f0da38f298a9cf0263b Mon Sep 17 00:00:00 2001 From: Kroonk Date: Thu, 21 May 2026 15:40:50 +0200 Subject: [PATCH] Handle browser-blocked status checks --- Brain.md | 4 +++- admin.html | 4 ++-- admin.js | 26 ++++++++++++++++++-------- app.js | 17 ++++++++++++----- index.html | 4 ++-- offline.html | 2 +- style.css | 9 +++++++++ sw.js | 6 +++--- 8 files changed, 50 insertions(+), 22 deletions(-) diff --git a/Brain.md b/Brain.md index f25d519..ffe4633 100644 --- a/Brain.md +++ b/Brain.md @@ -140,4 +140,6 @@ mischlabs/ - Erlaubter Admin in der statischen UI: - `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. -- 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. diff --git a/admin.html b/admin.html index bb65310..2a3f1c5 100644 --- a/admin.html +++ b/admin.html @@ -6,7 +6,7 @@ MischLabs Admin - +
@@ -57,6 +57,6 @@ - + diff --git a/admin.js b/admin.js index 7f6f5c5..9da538f 100644 --- a/admin.js +++ b/admin.js @@ -217,17 +217,20 @@ async function probe(url) { signal: controller.signal }); return { - ok: true, + status: 'online', ms: Math.round(performance.now() - started), detail: response.type === 'opaque' ? 'Erreichbar. HTTP-Details wegen Browser-CORS nicht lesbar.' : `HTTP ${response.status}` }; } catch (error) { + const isTimeout = error.name === 'AbortError'; return { - ok: false, + status: isTimeout ? 'offline' : 'unknown', 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 { window.clearTimeout(timeout); @@ -238,12 +241,20 @@ async function checkRow(row) { const service = config.services[Number(row.dataset.index)]; const diagnostic = row.querySelector('.diagnostic'); 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); - row.classList.toggle('is-online', result.ok); - row.classList.toggle('is-offline', !result.ok); - diagnostic.textContent = `${result.ok ? 'Online' : 'Fehler'} - ${result.ms} ms - ${result.detail}`; + row.classList.toggle('is-online', result.status === 'online'); + row.classList.toggle('is-offline', result.status === 'offline'); + 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() { @@ -315,4 +326,3 @@ downloadConfigButton.addEventListener('click', downloadConfig); boot().catch((error) => { loginMessage.textContent = `Adminseite konnte nicht starten: ${error.message}`; }); - diff --git a/app.js b/app.js index b3d6422..538ad2c 100644 --- a/app.js +++ b/app.js @@ -154,13 +154,19 @@ async function probeService(card) { }); card.classList.add('is-online'); card.classList.remove('is-offline'); + card.classList.remove('is-unknown'); card.querySelector('.status-dot').setAttribute('aria-label', 'Online'); return true; - } catch { - card.classList.add('is-offline'); + } catch (error) { + const isTimeout = error.name === 'AbortError'; + card.classList.toggle('is-offline', isTimeout); + card.classList.toggle('is-unknown', !isTimeout); card.classList.remove('is-online'); - card.querySelector('.status-dot').setAttribute('aria-label', 'Nicht erreichbar'); - return false; + card.querySelector('.status-dot').setAttribute( + 'aria-label', + isTimeout ? 'Nicht erreichbar' : 'Im Browser nicht pruefbar' + ); + return isTimeout ? false : null; } finally { window.clearTimeout(timeout); } @@ -169,11 +175,12 @@ async function probeService(card) { async function refreshStatus() { const results = await Promise.allSettled(cards.map(probeService)); 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); lastChecked.textContent = `Status geprueft: ${new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' - })}`; + })}${unknown ? `, ${unknown} im Browser nicht pruefbar` : ''}`; } function scheduleStatusRefresh() { diff --git a/index.html b/index.html index b4c10dd..39e06c2 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@ - +
@@ -73,6 +73,6 @@ Status wird im Hintergrund geprueft. - + diff --git a/offline.html b/offline.html index 71ecb21..70a715f 100644 --- a/offline.html +++ b/offline.html @@ -5,7 +5,7 @@ MischLabs - Offline - +
diff --git a/style.css b/style.css index 1eace67..c20f4d0 100644 --- a/style.css +++ b/style.css @@ -387,6 +387,11 @@ main { 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 { margin: 32px 0; padding: 18px; @@ -555,6 +560,10 @@ main { border-color: rgba(251, 113, 133, 0.36); } +.admin-row.is-unknown { + border-color: rgba(56, 189, 248, 0.32); +} + @media (max-width: 900px) { .hero { grid-template-columns: 1fr; diff --git a/sw.js b/sw.js index b27050a..e6f3a6b 100644 --- a/sw.js +++ b/sw.js @@ -1,10 +1,10 @@ -const CACHE_NAME = 'mischlabs-pwa-v4'; +const CACHE_NAME = 'mischlabs-pwa-v5'; const APP_SHELL = [ '/', '/index.html', '/offline.html', - '/style.css?v=10', - '/app.js?v=2', + '/style.css?v=11', + '/app.js?v=3', '/services.json?v=1', '/manifest.webmanifest?v=3', '/icons/icon-192.png',