/* ============================================================================ KalliLab — CUSTOM JAVASCRIPT Basiert auf LionCityGaming/homepage custom.js (MIT License) Anpassungen: KalliLab Tab-Namen, keine HA/EPL/Glance iFrames ============================================================================ */ const CONFIG = { STORAGE: { KEY: "lastFocusedTabId", }, TIMING: { RETRY_DELAY: 500, STANDARD_REFRESH: 1800000, // 30 Minuten QUICK_REFRESH: 60000, // 1 Minute RETRY_ON_ERROR: 30000, BATCH_DELAY: 100, }, SERVICES: { QUICK_REFRESH: [], // keine kritischen Quick-Refresh Widgets }, }; const RELOAD_BUTTON_SELECTORS = [ "#revalidate", '[data-testid="revalidate"]', ".reload-button", 'button[aria-label="Reload"]', '[role="button"][aria-label="Reload"]', ]; // Keine iFrame-Widgets in KalliLab const IFRAME_CONFIG = []; // Tab-Mapping für KalliLab Tabs (5 Tabs) const TAB_MAPPING = { "#ueberblick": ["#Überblick-tab", "#Überblick"], "#system": ["#System-tab", "#System"], "#sicherheit": ["#Sicherheit-tab", "#Sicherheit"], "#dienste": ["#Dienste-tab", "#Dienste"], "#backends": ["#Backends-tab", "#Backends"], "": ["#Überblick-tab", "#Überblick"], }; const state = { lastUpdate: new WeakMap(), currentFocusedTab: null, observers: { reloadButton: null, resize: null, }, }; const domCache = { myTab: null, tabContents: null, activeTabContent: null, initCache() { this.myTab = document.getElementById("myTab"); this.tabContents = document.querySelectorAll(".tabcontent"); this.updateActiveTab(); }, updateActiveTab() { this.activeTabContent = document.querySelector(".tabcontent.active"); }, clear() { this.myTab = null; this.tabContents = null; this.activeTabContent = null; }, }; const storage = { save(tabId) { try { localStorage.setItem(CONFIG.STORAGE.KEY, tabId); } catch (error) { console.warn("Storage save failed:", error); } }, get() { try { return localStorage.getItem(CONFIG.STORAGE.KEY); } catch (error) { console.warn("Storage retrieval failed:", error); return null; } }, }; function debounce(func, wait) { let timeout; return function executedFunction(...args) { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } function throttle(func, limit) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } }; } function removeReloadButton() { RELOAD_BUTTON_SELECTORS.forEach((selector) => { document.querySelectorAll(selector).forEach((element) => element.remove()); }); } function setupReloadButtonObserver() { if (state.observers.reloadButton) { return state.observers.reloadButton; } const observer = new MutationObserver( throttle((mutations) => { const hasAddedNodes = mutations.some((m) => m.addedNodes.length > 0); if (hasAddedNodes) { removeReloadButton(); } }, 100), ); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style"], }); state.observers.reloadButton = observer; return observer; } function updateServiceCard(card, data) { requestAnimationFrame(() => { const titleElement = card.querySelector(".card-title"); const statusElement = card.querySelector(".card-status"); if (titleElement && data.title) { titleElement.textContent = data.title; } if (statusElement) { statusElement.textContent = Array.isArray(data) ? `${data.length} items` : (data.status ?? (typeof data === "object" ? "Data received" : "")); } }); } function updateServiceCardError(card, error) { requestAnimationFrame(() => { const statusElement = card.querySelector(".card-status"); if (statusElement) { statusElement.textContent = error.message.includes("404") ? "Service unavailable" : "Error loading data"; statusElement.style.color = "red"; } }); } async function refreshService(card) { const apiEndpoint = card.dataset.apiEndpoint; if (!apiEndpoint) return; const serviceId = card.id || apiEndpoint; const now = Date.now(); const lastUpdateTime = state.lastUpdate.get(card) || 0; const isQuickRefreshService = CONFIG.SERVICES.QUICK_REFRESH.includes(serviceId); const minInterval = isQuickRefreshService ? CONFIG.TIMING.QUICK_REFRESH : CONFIG.TIMING.STANDARD_REFRESH; if (now - lastUpdateTime < minInterval) return; try { card.classList.add("updating"); const response = await fetch(apiEndpoint, { signal: AbortSignal.timeout(10000), }).then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }); updateServiceCard(card, response); state.lastUpdate.set(card, now); } catch (error) { console.error(`${serviceId} refresh failed:`, error); updateServiceCardError(card, error); state.lastUpdate.set(card, now - (minInterval - CONFIG.TIMING.RETRY_ON_ERROR)); } finally { card.classList.remove("updating"); } } async function batchUpdateServiceCards(cards) { const batchSize = 3; const batches = []; for (let i = 0; i < cards.length; i += batchSize) { batches.push(cards.slice(i, i + batchSize)); } for (const batch of batches) { await Promise.all(batch.map(refreshService)); if (batches.length > 1) { await new Promise((resolve) => setTimeout(resolve, CONFIG.TIMING.BATCH_DELAY)); } } } function handleTabFocusFromURL() { const hash = window.location.hash.toLowerCase(); const mapping = TAB_MAPPING[hash] || TAB_MAPPING[""]; const [tabSelector, contentSelector] = mapping; const tabToFocus = document.querySelector(tabSelector); const contentToShow = document.querySelector(contentSelector); if (tabToFocus) { setTabFocus(tabToFocus); storage.save(tabToFocus.id); domCache.tabContents.forEach((content) => { content.classList.remove("active"); content.style.display = "none"; }); if (contentToShow) { showTabContent(contentToShow); } } } function setTabFocus(tab) { requestAnimationFrame(() => { if (state.currentFocusedTab) { state.currentFocusedTab.classList.remove("tab-focused"); } state.currentFocusedTab = tab; state.currentFocusedTab.classList.add("tab-focused"); }); } function showTabContent(contentElement) { if (!contentElement) return; contentElement.classList.add("active"); contentElement.style.display = "block"; domCache.updateActiveTab(); } async function preloadAllTabs() { const tabContents = document.querySelectorAll(".tab-pane"); const serviceCards = new Set(); tabContents.forEach((tab) => { tab.querySelectorAll(".service-card").forEach((card) => serviceCards.add(card)); }); await batchUpdateServiceCards(Array.from(serviceCards)); } function setupPeriodicRefresh() { const debouncedRefresh = debounce(async () => { if (domCache.activeTabContent) { const cards = domCache.activeTabContent.querySelectorAll( ".service-card[data-api-endpoint]", ); await batchUpdateServiceCards(Array.from(cards)); } }, 250); setInterval( debouncedRefresh, Math.min(CONFIG.TIMING.QUICK_REFRESH, CONFIG.TIMING.STANDARD_REFRESH), ); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { debouncedRefresh(); } }); domCache.myTab?.addEventListener("click", (event) => { if (event.target.matches('[id$="-tab"]')) { debouncedRefresh(); } }); } function initializeTabFocus() { // KalliLab Tab-Selektoren const tabs = document.querySelectorAll( "#Überblick-tab, #System-tab, #Sicherheit-tab, #Dienste-tab, #Backends-tab", ); handleTabFocusFromURL(); if (!window.location.hash) { const savedTabId = storage.get(); const savedTab = savedTabId && document.getElementById(savedTabId); if (savedTab) { setTabFocus(savedTab); } else { const activeTab = document.querySelector(".tabcontent.active"); const correspondingTab = activeTab && document.querySelector(`[aria-controls="${activeTab.id}"]`); if (correspondingTab) { setTabFocus(correspondingTab); } } } tabs.forEach((tab) => { const handleTabAction = function () { setTabFocus(this); storage.save(this.id); }; tab.addEventListener("click", handleTabAction); tab.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); this.click(); handleTabAction.call(this); } }); }); window.addEventListener("beforeunload", () => { if (state.currentFocusedTab) { storage.save(state.currentFocusedTab.id); } }); } function initializeEverything() { removeReloadButton(); setupReloadButtonObserver(); [100, 500, 1000].forEach((delay) => { setTimeout(removeReloadButton, delay); }); const hasRequiredElements = document.querySelector("#myTab") && document.querySelector(".service-card"); if (hasRequiredElements) { domCache.initCache(); initializeTabFocus(); preloadAllTabs(); setupPeriodicRefresh(); } else { setTimeout(initializeEverything, CONFIG.TIMING.RETRY_DELAY); } window.addEventListener("orientationchange", () => { setTimeout(removeReloadButton, 100); }); } document.addEventListener("DOMContentLoaded", removeReloadButton); window.addEventListener("load", initializeEverything); if (typeof window.htmlLoaded === "function") { const originalHtmlLoaded = window.htmlLoaded; window.htmlLoaded = () => { originalHtmlLoaded(); initializeEverything(); }; } if ("ontouchstart" in window) { window.addEventListener("touchend", () => { setTimeout(removeReloadButton, 100); }, { passive: true }); } function cleanup() { if (state.observers.reloadButton) { state.observers.reloadButton.disconnect(); state.observers.reloadButton = null; } if (state.observers.resize) { state.observers.resize.disconnect(); state.observers.resize = null; } domCache.clear(); state.currentFocusedTab = null; } window.addEventListener("unload", cleanup);