407 lines
10 KiB
JavaScript
Executable File
407 lines
10 KiB
JavaScript
Executable File
/* ============================================================================
|
|
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); |