Handle browser-blocked status checks
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
This commit is contained in:
4
Brain.md
4
Brain.md
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
26
admin.js
26
admin.js
@@ -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
17
app.js
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
6
sw.js
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user