// Fetch services.xml and render the service cards with logos. (async function(){ const container = document.getElementById('services-container'); const searchInput = document.getElementById('search-input'); const host = window.location.hostname; let allServices = []; // Store all service elements for filtering try{ const res = await fetch('/services.xml', {cache: 'no-cache'}); if(!res.ok){ throw new Error('Failed to load services.xml'); } const text = await res.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'application/xml'); // Check for XML parsing errors const parseError = doc.querySelector('parsererror'); if(parseError){ throw new Error('XML parsing error: ' + parseError.textContent); } // Check if we have groups or just services const groups = Array.from(doc.getElementsByTagName('group')); const hasGroups = groups.length > 0; console.log('Found', groups.length, 'groups'); if(hasGroups){ // Render grouped services groups.forEach(group => { const groupName = group.getAttribute('name') || 'Services'; const services = Array.from(group.getElementsByTagName('service')); console.log('Group:', groupName, 'has', services.length, 'services'); if(services.length === 0) return; // Create group section const groupSection = document.createElement('section'); groupSection.className = 'service-group'; const groupHeader = document.createElement('h2'); groupHeader.className = 'group-header'; groupHeader.textContent = groupName; groupSection.appendChild(groupHeader); const grid = document.createElement('div'); grid.className = 'grid'; services.forEach(s => { const card = createServiceCard(s, host, allServices); grid.appendChild(card); }); groupSection.appendChild(grid); container.appendChild(groupSection); }); } else { // Fallback: render ungrouped services const services = Array.from(doc.getElementsByTagName('service')); if(services.length === 0){ container.innerHTML = '
No services found in services.xml
'; return; } const grid = document.createElement('div'); grid.className = 'grid'; grid.id = 'services-grid'; services.forEach(s => { const card = createServiceCard(s, host, allServices); grid.appendChild(card); }); container.appendChild(grid); } // Function to create a service card function createServiceCard(s, host, allServices) { const name = s.getAttribute('name') || s.getAttribute('id') || 'unknown'; const proto = s.getAttribute('proto') || 'http'; const port = s.getAttribute('port') || ''; const logo = s.getAttribute('logo') || ''; const hostAttr = s.getAttribute('host'); // optional public hostname or full URL const manualStatus = s.getAttribute('status'); // optional: 'online', 'offline', 'maintenance' const checkHealth = s.getAttribute('check-health') !== 'false'; // default true, set to false to disable // Build href: prefer explicit host attribute when present. let href = ''; if(hostAttr){ if(/^https?:\/\//i.test(hostAttr)){ href = hostAttr; } else { const hasPortInHost = /:\d+$/.test(hostAttr); if(hasPortInHost){ href = `${proto}://${hostAttr}`; } else { href = `https://${hostAttr}`; } } } else { let portPart = ''; if(port && !((proto==='http'&&port==='80')||(proto==='https'&&port==='443'))){ portPart = ':'+port; } href = `${proto}://${host}${portPart}`; } const a = document.createElement('a'); a.className = 'card'; a.href = href; a.target = '_blank'; a.rel = 'noreferrer'; a.dataset.serviceName = name.toLowerCase(); // For search filtering const img = document.createElement('img'); img.className = 'logo'; img.src = logo ? ('/logos/'+logo) : '/logos/default.svg'; img.alt = name + ' logo'; const span = document.createElement('div'); span.className = 'label'; span.textContent = `${name}`; a.appendChild(img); a.appendChild(span); // Add status indicator - will be updated by health check const statusDot = document.createElement('span'); statusDot.className = 'status-dot status-checking'; statusDot.title = 'Checking status...'; let currentStatus = 'checking'; // If maintenance mode is set manually, use that and skip health check if(manualStatus === 'maintenance'){ statusDot.className = 'status-dot status-maintenance'; statusDot.title = 'Status: maintenance'; currentStatus = 'maintenance'; a.appendChild(statusDot); } else if(checkHealth && href){ // Show checking indicator initially a.appendChild(statusDot); // Perform health check (async function performHealthCheck(){ try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout const response = await fetch(href, { method: 'HEAD', mode: 'no-cors', // Allow cross-origin requests cache: 'no-cache', signal: controller.signal }); clearTimeout(timeoutId); // In no-cors mode, opaque responses mean the server responded currentStatus = 'online'; statusDot.className = 'status-dot status-online'; statusDot.title = 'Status: online'; } catch(err) { // Connection failed or timed out currentStatus = 'offline'; statusDot.className = 'status-dot status-offline'; statusDot.title = 'Status: offline'; } })(); } else if(manualStatus){ // Use manual status if health check is disabled statusDot.className = `status-dot status-${manualStatus}`; statusDot.title = `Status: ${manualStatus}`; currentStatus = manualStatus; a.appendChild(statusDot); } // Add info button if both hostname and port are available if((hostAttr || host) && port){ const infoBtn = document.createElement('button'); infoBtn.className = 'info-btn'; infoBtn.title = 'Connection details'; infoBtn.innerHTML = 'ⓘ'; infoBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const displayHost = hostAttr || host; const details = `Service: ${name}\nHost: ${displayHost}\nPort: ${port}\nProtocol: ${proto}`; alert(details); }; a.appendChild(infoBtn); } allServices.push(a); return a; } // Export allServices for use by search and keyboard navigation window.servicesData = { allServices, container }; }catch(err){ console.error(err); container.innerHTML = 'Error loading services.xml — see console for details.
'; } })();