| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- const dataNode = document.getElementById('initial-status');
- if (dataNode) {
- let currentStatus = JSON.parse(dataNode.textContent || '{}');
- let activeMachine = 'all';
- const basePath = dataNode.dataset.basePath || '';
- 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 appUrl = (path) => `${basePath}${path}`;
- 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 = [
- `<button class="chip ${activeMachine === 'all' ? 'chip--active' : ''}" data-machine="all">Alle</button>`,
- ...machines.map(
- (machine) =>
- `<button class="chip ${activeMachine === machine.id ? 'chip--active' : ''}" data-machine="${escapeHtml(
- machine.id
- )}">${escapeHtml(machine.name)}</button>`
- ),
- ];
- 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 `
- <article class="slot-card slot-card--${escapeHtml(state)}">
- <div class="slot-card__head">
- <div>
- <p class="slot-card__label">${escapeHtml(slot.slot_label || slot.sensor_id)}</p>
- <h3>${escapeHtml(slot.product_name || 'Nicht zugeordnet')}</h3>
- </div>
- <span class="status-pill status-pill--${escapeHtml(state)}">${stateLabel}</span>
- </div>
- <div class="slot-card__body">
- <div class="fill-tube" style="--fill:${fillPercent}%">
- <div class="fill-tube__liquid" style="height:${fillPercent}%"></div>
- <div class="fill-tube__gloss"></div>
- </div>
- <div class="slot-card__metrics">
- <p><strong>${fillPercent}%</strong> Füllstand</p>
- <p><strong>${units}</strong> / ${maxUnits} Flaschen</p>
- <p>Alarm unter <strong>${slot.alert_below_units ?? 0}</strong></p>
- <p>Messwert: <strong>${slot.distance_mm ?? '–'} mm</strong></p>
- <p>Update: <strong>${formatTime(slot.measured_at)}</strong></p>
- </div>
- </div>
- </article>
- `;
- };
- const renderMachines = () => {
- const machines = (currentStatus.machines || []).filter(
- (machine) => activeMachine === 'all' || machine.id === activeMachine
- );
- if (!machines.length) {
- gridNode.innerHTML = '<p class="empty-state">Keine Automaten für die aktuelle Auswahl gefunden.</p>';
- return;
- }
- gridNode.innerHTML = machines
- .map(
- (machine) => `
- <section class="machine-panel">
- <div class="machine-panel__head">
- <div>
- <p class="eyebrow">Automat</p>
- <h2>${escapeHtml(machine.name)}</h2>
- </div>
- <p>${escapeHtml(machine.location || 'Kein Standort hinterlegt')}</p>
- </div>
- <div class="slot-grid">
- ${(machine.slots || []).map(slotCard).join('')}
- </div>
- </section>
- `
- )
- .join('');
- };
- const renderAlerts = () => {
- const alerts = currentStatus.alerts || [];
- if (!alerts.length) {
- alertNode.innerHTML =
- '<p class="empty-state">Noch keine Zustandswechsel registriert.</p>';
- 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 `
- <article class="alert-entry alert-entry--${escapeHtml(stateClass)}">
- <div>
- <p class="alert-entry__title">${stateText}: ${escapeHtml(
- payload.machine_name || payload.machine_id || 'Automat'
- )} / ${escapeHtml(payload.slot_label || payload.sensor_id || 'Fach')}</p>
- <p>${escapeHtml(payload.product_name || 'Ohne Produktname')} • Bestand ${
- payload.units_estimated ?? '–'
- } / ${payload.max_units ?? '–'} • ${payload.fill_percent ?? '–'}%</p>
- </div>
- <time>${formatTime(entry.created_at)}</time>
- </article>
- `;
- })
- .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(appUrl('/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);
- }
|