const dataNode = document.getElementById('initial-status'); if (dataNode) { let currentStatus = JSON.parse(dataNode.textContent || '{}'); let activeMachine = 'all'; const gridNode = document.getElementById('machine-grid'); const filterNode = document.getElementById('machine-filter'); const alertNode = document.getElementById('alert-list'); const generatedAtNode = document.getElementById('generated-at'); const escapeHtml = (value) => String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); const formatTime = (isoValue) => { if (!isoValue) { return 'Noch keine Messung'; } const parsed = new Date(isoValue); if (Number.isNaN(parsed.getTime())) { return isoValue; } return parsed.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); }; const renderFilters = () => { const machines = currentStatus.machines || []; const buttons = [ ``, ...machines.map( (machine) => `` ), ]; filterNode.innerHTML = buttons.join(''); }; const slotCard = (slot) => { const fillPercent = slot.fill_percent ?? 0; const units = slot.units_estimated ?? '–'; const maxUnits = slot.max_units ?? '–'; const state = slot.state || 'unknown'; const stateLabel = state === 'critical' ? 'Kritisch' : state === 'ok' ? 'Stabil' : 'Unbekannt'; return `

${escapeHtml(slot.slot_label || slot.sensor_id)}

${escapeHtml(slot.product_name || 'Nicht zugeordnet')}

${stateLabel}

${fillPercent}% Fuellstand

${units} / ${maxUnits} Flaschen

Alarm unter ${slot.alert_below_units ?? 0}

Messwert: ${slot.distance_mm ?? '–'} mm

Update: ${formatTime(slot.measured_at)}

`; }; const renderMachines = () => { const machines = (currentStatus.machines || []).filter( (machine) => activeMachine === 'all' || machine.id === activeMachine ); if (!machines.length) { gridNode.innerHTML = '

Keine Automaten fuer die aktuelle Auswahl gefunden.

'; return; } gridNode.innerHTML = machines .map( (machine) => `

Automat

${escapeHtml(machine.name)}

${escapeHtml(machine.location || 'Kein Standort hinterlegt')}

${(machine.slots || []).map(slotCard).join('')}
` ) .join(''); }; const renderAlerts = () => { const alerts = currentStatus.alerts || []; if (!alerts.length) { alertNode.innerHTML = '

Noch keine Zustandswechsel registriert.

'; return; } alertNode.innerHTML = alerts .slice(0, 12) .map((entry) => { const payload = entry.payload || {}; const stateClass = payload.event === 'critical' ? 'critical' : 'ok'; const stateText = payload.event === 'critical' ? 'Alarm' : 'Entwarnung'; return `

${stateText}: ${escapeHtml( payload.machine_name || payload.machine_id || 'Automat' )} / ${escapeHtml(payload.slot_label || payload.sensor_id || 'Fach')}

${escapeHtml(payload.product_name || 'Ohne Produktname')} • Bestand ${ payload.units_estimated ?? '–' } / ${payload.max_units ?? '–'} • ${payload.fill_percent ?? '–'}%

`; }) .join(''); }; const updateSummary = () => { const summary = currentStatus.summary || {}; Object.entries(summary).forEach(([key, value]) => { const node = document.querySelector(`[data-summary="${key}"]`); if (node) { node.textContent = value; } }); if (generatedAtNode) { generatedAtNode.textContent = formatTime(currentStatus.generated_at); } }; const render = () => { renderFilters(); renderMachines(); renderAlerts(); updateSummary(); }; filterNode.addEventListener('click', (event) => { const target = event.target.closest('[data-machine]'); if (!target) { return; } activeMachine = target.dataset.machine || 'all'; render(); }); const refresh = async () => { try { const response = await fetch('/api/v1/status.php', { cache: 'no-store' }); if (!response.ok) { return; } currentStatus = await response.json(); render(); } catch (error) { console.error('Status-Aktualisierung fehlgeschlagen', error); } }; render(); const refreshSeconds = currentStatus.app?.dashboard_refresh_seconds || 15; window.setInterval(refresh, refreshSeconds * 1000); }