/** * Service Data Loader * This script loads services from services-data.json and provides helper functions * to render them in different HTML structures for gw.html and ugh.html * * NOTE: Services are loaded from JSON at runtime (expected to be served over HTTP). * Edit `services-data.json` to update the service directory. */ // Capture data-services-url at load time (for archived pages in subdirs) const SERVICES_JSON_URL = (function () { const s = document.currentScript; return (s && s.dataset.servicesUrl) ? s.dataset.servicesUrl : 'services-data.json'; })(); // Load and cache the services data let servicesData = null; async function loadServicesData() { if (servicesData) return servicesData; try { const response = await fetch(SERVICES_JSON_URL); if (response.ok) { servicesData = await response.json(); return servicesData; } console.warn(`Failed to load ${SERVICES_JSON_URL}: HTTP ${response.status}`); } catch (err) { console.warn(`Failed to load ${SERVICES_JSON_URL}:`, err); } // No data available (e.g. file:// protocol or missing JSON) servicesData = { services: [] }; return servicesData; } // Group services by category function groupByCategory(services) { const grouped = {}; services.forEach(service => { if (!grouped[service.category]) { grouped[service.category] = []; } grouped[service.category].push(service); }); return grouped; } // Split services into active (Ugh, GS, GW) and archived function splitActiveArchived(services) { const active = []; const archived = []; services.forEach(service => { if (service.active !== false) { active.push(service); } else { archived.push(service); } }); return { active, archived }; } // Escape for HTML text content (defense-in-depth against XSS from service data) function escapeHtml(str) { if (typeof str !== 'string') return ''; return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // Escape for HTML attribute values (e.g. href) function escapeAttr(str) { if (typeof str !== 'string') return ''; return str.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(//g, '>'); } function isFlaggedDown(service) { return service && (service.status === 'down' || service.status === 'maintenance'); } // Render one service list (used for both active and archived blocks) function renderServiceGroupHtml(grouped, options = {}) { let html = ''; const { showArchivedStyle = false } = options; for (const [category, services] of Object.entries(grouped)) { html += `