const CONFIG_URL = '/services.json?v=1'; const LOCAL_CONFIG_KEY = 'mischlabs.dashboard.config'; const icons = { shield: '', folder: '', lock: '', film: '', headphones: '', book: '', git: '', chess: '', user: '' }; let cards = []; let sections = []; let config = null; let activeFilter = 'all'; let categoryById = new Map(); const searchInput = document.querySelector('#serviceSearch'); const segments = [...document.querySelectorAll('.segment')]; const emptyState = document.querySelector('#emptyState'); const onlineCount = document.querySelector('#onlineCount'); const serviceCount = document.querySelector('#serviceCount'); const lastChecked = document.querySelector('#lastChecked'); const dashboardSections = document.querySelector('#dashboardSections'); function normalize(value) { return String(value || '').toLowerCase().trim(); } function getLocalConfig() { try { const raw = window.localStorage.getItem(LOCAL_CONFIG_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } async function loadConfig() { const response = await fetch(CONFIG_URL, { cache: 'no-store' }); const baseConfig = await response.json(); return getLocalConfig() || baseConfig; } function serviceSearchText(service) { return [ service.name, service.description, service.domain, service.category, service.keywords ].join(' '); } function createServiceCard(service) { const category = categoryById.get(service.category); const card = document.createElement('a'); card.href = service.url; card.target = '_blank'; card.rel = 'noopener'; card.className = 'card service-card'; card.dataset.category = service.category; card.dataset.url = service.url; card.dataset.name = serviceSearchText(service); card.innerHTML = `
${category?.label || 'Dienst'}

${service.name}

${service.description}

${service.domain} `; return card; } function renderDashboard(nextConfig) { dashboardSections.innerHTML = ''; serviceCount.textContent = String(nextConfig.services.length); categoryById = new Map(nextConfig.categories.map((category) => [category.id, category])); const section = document.createElement('section'); section.className = 'section service-overview'; section.innerHTML = `

Dashboard

Alle Dienste

${nextConfig.services.length} Dienste
`; const grid = section.querySelector('.grid'); nextConfig.services.forEach((service) => grid.appendChild(createServiceCard(service))); dashboardSections.appendChild(section); cards = [...document.querySelectorAll('.service-card')]; sections = [...document.querySelectorAll('.section')]; updateOverviewHeading(nextConfig.services.length); } function updateOverviewHeading(visibleCards) { const activeCategory = categoryById.get(activeFilter); const term = normalize(searchInput.value); const kicker = document.querySelector('#overviewKicker'); const title = document.querySelector('#overviewTitle'); const count = document.querySelector('#overviewCount'); if (!kicker || !title || !count) return; kicker.textContent = activeCategory ? activeCategory.label : 'Dashboard'; title.textContent = activeCategory ? activeCategory.title : 'Alle Dienste'; count.textContent = `${visibleCards} ${visibleCards === 1 ? 'Dienst' : 'Dienste'}${term ? ' gefunden' : ''}`; } function updateVisibility() { const term = normalize(searchInput.value); let visibleCards = 0; cards.forEach((card) => { const categoryMatches = activeFilter === 'all' || card.dataset.category === activeFilter; const searchMatches = !term || normalize(card.dataset.name).includes(term); const isVisible = categoryMatches && searchMatches; card.hidden = !isVisible; if (isVisible) visibleCards += 1; }); sections.forEach((section) => { const hasVisibleCards = [...section.querySelectorAll('.service-card')].some((card) => !card.hidden); section.hidden = !hasVisibleCards; }); updateOverviewHeading(visibleCards); emptyState.hidden = visibleCards !== 0; } function setActiveFilter(nextFilter) { activeFilter = nextFilter; segments.forEach((segment) => { const isActive = segment.dataset.filter === nextFilter; segment.classList.toggle('is-active', isActive); segment.setAttribute('aria-selected', String(isActive)); }); updateVisibility(); } async function probeService(card) { const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), 4500); try { await fetch(card.dataset.url, { method: 'GET', mode: 'no-cors', cache: 'no-store', signal: controller.signal }); 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 (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', isTimeout ? 'Nicht erreichbar' : 'Im Browser nicht pruefbar' ); return isTimeout ? false : null; } finally { window.clearTimeout(timeout); } } 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() { const run = () => refreshStatus().catch(() => { onlineCount.textContent = '--'; lastChecked.textContent = 'Status aktuell nicht verfuegbar.'; }); if ('requestIdleCallback' in window) { window.requestIdleCallback(run, { timeout: 2000 }); } else { window.setTimeout(run, 600); } } segments.forEach((segment) => { segment.addEventListener('click', () => setActiveFilter(segment.dataset.filter)); }); searchInput.addEventListener('input', updateVisibility); if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').catch(() => {}); }); } loadConfig() .then((nextConfig) => { config = nextConfig; renderDashboard(config); updateVisibility(); scheduleStatusRefresh(); }) .catch(() => { dashboardSections.innerHTML = '

Dashboard-Konfiguration konnte nicht geladen werden.

'; onlineCount.textContent = '--'; });