${service.name}
${service.domain}
Noch nicht geprueft.
`).join('');
serviceEditor.querySelectorAll('select,input').forEach((input) => {
input.addEventListener('change', () => {
const row = input.closest('.admin-row');
const service = config.services[Number(row.dataset.index)];
service[input.dataset.field] = input.value;
});
});
serviceEditor.querySelectorAll('.check-one').forEach((button) => {
button.addEventListener('click', () => checkRow(button.closest('.admin-row')));
});
}
async function probe(url) {
const started = performance.now();
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 6000);
try {
const response = await fetch(url, {
mode: 'no-cors',
cache: 'no-store',
signal: controller.signal
});
return {
ok: true,
ms: Math.round(performance.now() - started),
detail: response.type === 'opaque'
? 'Erreichbar. HTTP-Details wegen Browser-CORS nicht lesbar.'
: `HTTP ${response.status}`
};
} catch (error) {
return {
ok: false,
ms: Math.round(performance.now() - started),
detail: error.name === 'AbortError' ? 'Timeout nach 6 Sekunden.' : error.message
};
} finally {
window.clearTimeout(timeout);
}
}
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');
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}`;
}
function saveLocal() {
localStorage.setItem(LOCAL_CONFIG_KEY, JSON.stringify(config, null, 2));
}
function downloadConfig() {
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'services.json';
link.click();
URL.revokeObjectURL(url);
}
async function boot() {
await handleCallback();
tokenSet = getStoredTokens();
if (!isTokenValid(tokenSet)) {
loginPanel.hidden = false;
adminPanel.hidden = true;
loginButton.hidden = false;
logoutButton.hidden = true;
return;
}
const claims = authorize(tokenSet);
if (!claims) return;
config = await loadConfig();
loginPanel.hidden = true;
adminPanel.hidden = false;
loginButton.hidden = true;
logoutButton.hidden = false;
adminUser.textContent = `Angemeldet als ${claims.preferred_username || claims.email}`;
renderEditor();
}
loginButton.addEventListener('click', login);
logoutButton.addEventListener('click', () => {
const idToken = tokenSet?.id_token;
clearTokens();
const params = new URLSearchParams({
client_id: CLIENT_ID,
post_logout_redirect_uri: REDIRECT_URI
});
if (idToken) params.set('id_token_hint', idToken);
window.location.href = `${AUTHORITY}/protocol/openid-connect/logout?${params}`;
});
checkAllButton.addEventListener('click', () => {
[...serviceEditor.querySelectorAll('.admin-row')].forEach((row) => checkRow(row));
});
saveLocalButton.addEventListener('click', () => {
saveLocal();
window.alert('Lokal gespeichert. Das Dashboard in diesem Browser nutzt diese Aenderungen.');
});
resetLocalButton.addEventListener('click', () => {
localStorage.removeItem(LOCAL_CONFIG_KEY);
window.location.reload();
});
downloadConfigButton.addEventListener('click', downloadConfig);
boot().catch((error) => {
loginMessage.textContent = `Adminseite konnte nicht starten: ${error.message}`;
});