/** * PredictiveAI Dashboard — Application Logic * All API wiring preserved. Visual rendering updated for editorial theme. */ const API = '/api'; const state = { currentView: 'overview', reports: [], fleet: [], schedule: [] }; const els = { loader: document.getElementById('app-loader'), navItems: document.querySelectorAll('.nav-link'), views: document.querySelectorAll('.view'), toasts: document.getElementById('toast-container'), pulse: document.getElementById('system-pulse'), }; // ─── Init ─────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { els.navItems.forEach(n => n.addEventListener('click', e => { e.preventDefault(); switchView(n.dataset.view); })); try { initOverview(); initFleet(); initPredict(); initSchedule(); } catch (e) { console.error('Init error:', e); } setTimeout(() => { if (els.loader) els.loader.classList.add('hidden'); }, 500); }); function switchView(id) { if (state.currentView === id) return; els.navItems.forEach(n => n.classList.toggle('active', n.dataset.view === id)); els.views.forEach(v => v.classList.toggle('active', v.id === `view-${id}`)); state.currentView = id; if (id === 'overview') refreshOverview(); if (id === 'fleet') refreshFleet(); if (id === 'schedule') loadSchedule(); if (id === 'reports') loadReportsList(); } // ─── Utilities ────────────────────────────────────────── function toast(msg, type = 'info') { if (!els.toasts) return; const t = document.createElement('div'); t.className = `toast toast--${type}`; t.textContent = msg; els.toasts.appendChild(t); setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 3500); } function setPulse(online) { if (!els.pulse) return; const dot = els.pulse.querySelector('.pulse-dot'); const lbl = els.pulse.querySelector('.pulse-label'); if (dot) dot.style.background = online ? 'var(--ok)' : 'var(--danger)'; if (lbl) lbl.textContent = online ? 'System online' : 'System offline'; els.pulse.style.color = online ? 'var(--ok)' : 'var(--danger)'; } // Chart.js global overrides for light editorial theme if (typeof Chart !== 'undefined') { Chart.defaults.color = '#78716c'; Chart.defaults.borderColor = '#e7e5e4'; Chart.defaults.font.family = "'DM Sans', sans-serif"; } // ─── 1. Overview ──────────────────────────────────────── let chartHealth = null, chartRulDist = null; function initOverview() { const btn = document.getElementById('btn-refresh-overview'); if (btn) btn.addEventListener('click', refreshOverview); const hEl = document.getElementById('chart-health-dist'); if (hEl && typeof Chart !== 'undefined') { chartHealth = new Chart(hEl.getContext('2d'), { type: 'doughnut', data: { labels: ['Healthy', 'At risk', 'Critical'], datasets: [{ data: [0,0,0], backgroundColor: ['#059669','#d97706','#dc2626'], borderWidth: 0, hoverOffset: 2 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '72%', plugins: { legend: { position: 'right', labels: { color: '#44403c', font: { size: 11 }, padding: 14 } } } } }); } const rEl = document.getElementById('chart-rul-dist'); if (rEl && typeof Chart !== 'undefined') { chartRulDist = new Chart(rEl.getContext('2d'), { type: 'bar', data: { labels: ['0-24 h', '24-72 h', '72-150 h', '150+ h'], datasets: [{ label: 'Machines', data: [0,0,0,0], backgroundColor: 'rgba(5, 150, 105, 0.45)', borderRadius: 2 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, grid: { color: '#e7e5e4' }, ticks: { color: '#78716c' } }, x: { grid: { display: false }, ticks: { color: '#78716c' } } }, plugins: { legend: { display: false } } } }); } refreshOverview(); } async function refreshOverview() { const btn = document.getElementById('btn-refresh-overview'); try { if (btn) btn.textContent = 'Loading...'; const [fleetRes, statusRes] = await Promise.all([ fetch(`${API}/fleet/rul`), fetch(`${API}/status`) ]); if (!fleetRes.ok || !statusRes.ok) throw new Error('API error'); const fleet = await fleetRes.json(); const status = await statusRes.json(); if (chartHealth) { chartHealth.data.datasets[0].data = [fleet.summary.healthy, fleet.summary.at_risk, fleet.summary.critical]; chartHealth.update(); } const b = [0,0,0,0]; fleet.fleet.forEach(m => { if (m.rul_hours<=24) b[0]++; else if (m.rul_hours<=72) b[1]++; else if (m.rul_hours<=150) b[2]++; else b[3]++; }); if (chartRulDist) { chartRulDist.data.datasets[0].data = b; chartRulDist.update(); } setText('kpi-total', fleet.summary.total_machines); setText('kpi-healthy', status.healthy_jobs); setText('kpi-atrisk', fleet.summary.at_risk); setText('kpi-critical', fleet.summary.critical); setPulse(true); if (btn) btn.textContent = 'Refresh'; } catch (e) { console.error(e); toast('Failed to connect to backend', 'error'); setPulse(false); if (btn) btn.textContent = 'Refresh'; } } function setText(id, v) { const el = document.getElementById(id); if (el) el.textContent = v; } // ─── 2. Fleet ─────────────────────────────────────────── function initFleet() { const btn = document.getElementById('btn-refresh-fleet'); const f1 = document.getElementById('fleet-filter'); const f2 = document.getElementById('fleet-model-filter'); if (btn) btn.addEventListener('click', refreshFleet); if (f1) f1.addEventListener('change', renderFleet); if (f2) f2.addEventListener('change', renderFleet); } async function refreshFleet() { try { const res = await fetch(`${API}/fleet/rul`); if (!res.ok) throw new Error(); const data = await res.json(); state.fleet = data.fleet; const t = data.summary.total_machines; setText('fleet-total', t); setWidth('bar-critical', (data.summary.critical / t) * 100); setWidth('bar-warning', (data.summary.at_risk / t) * 100); setWidth('bar-healthy', (data.summary.healthy / t) * 100); renderFleet(); toast('Fleet data updated', 'success'); } catch (e) { console.error(e); toast('Failed to load fleet data', 'error'); const g = document.getElementById('machine-grid'); if (g) g.innerHTML = '
Unable to connect to backend.
'; } } function setWidth(id, pct) { const el = document.getElementById(id); if (el) el.style.width = `${pct}%`; } function renderFleet() { const grid = document.getElementById('machine-grid'); if (!grid) return; const sf = valOf('fleet-filter', 'all'); const mf = valOf('fleet-model-filter', 'all'); let list = state.fleet; if (sf !== 'all') list = list.filter(m => m.status === sf); if (mf !== 'all') list = list.filter(m => m.model === mf); if (!list.length) { grid.innerHTML = '
No machines match filters.
'; return; } grid.innerHTML = list.map(m => { const s = m.status === 'Critical' ? 'crit' : m.status === 'At Risk' ? 'warn' : 'ok'; return `
M-${m.machine_id} ${m.model}
${m.status}
Failure risk ${(m.failure_probability*100).toFixed(1)}%
${m.rul_hours.toFixed(1)}h
V ${m.sensors.volt}
R ${m.sensors.rotate}
P ${m.sensors.pressure}
Vib ${m.sensors.vibration}
`; }).join(''); } function valOf(id, fallback) { const el = document.getElementById(id); return el ? el.value : fallback; } // ─── 3. Diagnostics ───────────────────────────────────── let gaugeChart = null, rulTrendChart = null; function initPredict() { const form = document.getElementById('predict-form'); if (form) form.addEventListener('submit', runPredict); document.querySelectorAll('.btn-preset').forEach(b => b.addEventListener('click', e => { e.preventDefault(); const p = b.dataset.preset; const vals = { healthy: { volt: 170.5, rotate: 440.2, pressure: 100.1, vib: 40.2 }, atrisk: { volt: 185.0, rotate: 340.0, pressure: 100.0, vib: 45.0 }, critical: { volt: 210.5, rotate: 520.1, pressure: 115.5, vib: 55.2 }, }[p]; if (vals) { setVal('inp-volt', vals.volt); setVal('inp-rotate', vals.rotate); setVal('inp-pressure', vals.pressure); setVal('inp-vib', vals.vib); } })); const gEl = document.getElementById('chart-gauge'); if (gEl && typeof Chart !== 'undefined') { gaugeChart = new Chart(gEl.getContext('2d'), { type: 'doughnut', data: { datasets: [{ data: [0,100], backgroundColor: ['#059669','#f5f5f4'], borderWidth: 0, circumference: 180, rotation: 270, cutout: '82%' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { tooltip: { enabled: false } } } }); } const tEl = document.getElementById('chart-rul-trend'); if (tEl && typeof Chart !== 'undefined') { rulTrendChart = new Chart(tEl.getContext('2d'), { type: 'line', data: { labels: ['T-4','T-3','T-2','T-1','Now'], datasets: [{ data: [100,100,100,100,100], borderColor: '#059669', tension: .3, borderWidth: 1.5, pointRadius: 0 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { display: false }, y: { display: false, min: 0, max: 100 } }, plugins: { legend: { display: false } } } }); } } function setVal(id, v) { const el = document.getElementById(id); if (el) el.value = v; } async function runPredict(e) { e.preventDefault(); const body = { machineID: parseInt(valOf('inp-machine', '3')), age: parseFloat(valOf('inp-age', '5')), volt: parseFloat(valOf('inp-volt', '170')), rotate: parseFloat(valOf('inp-rotate', '440')), pressure: parseFloat(valOf('inp-pressure', '100')), vibration: parseFloat(valOf('inp-vib', '40')) }; hide('predict-empty'); hide('results-top'); hide('ai-insight-panel'); show('predict-loading'); try { const res = await fetch(`${API}/predict`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error(); renderDiagnostic(await res.json()); } catch (err) { console.error(err); toast('Inference engine failed', 'error'); hide('predict-loading'); show('predict-empty'); } } function renderDiagnostic(d) { hide('predict-loading'); show('results-top', 'grid'); show('ai-insight-panel'); const pct = (d.failure_probability || 0) * 100; const rul = d.rul || 0; let clr = '#059669', cls = 'status-indicator--ok'; if (d.status === 'Critical') { clr = '#dc2626'; cls = 'status-indicator--crit'; } else if (d.status === 'At Risk') { clr = '#d97706'; cls = 'status-indicator--warn'; } setText('gauge-value', `${pct.toFixed(1)}%`); if (gaugeChart) { gaugeChart.data.datasets[0].data = [pct, 100-pct]; gaugeChart.data.datasets[0].backgroundColor[0] = clr; gaugeChart.update(); } const badge = document.getElementById('status-badge'); if (badge) { badge.textContent = d.status; badge.className = `status-indicator ${cls}`; } setText('rul-value', rul.toFixed(1)); const bar = document.getElementById('rul-bar'); if (bar) { bar.style.width = `${Math.max(0, Math.min(100, rul))}%`; bar.style.background = clr; } if (rulTrendChart) { const h = [rul+10, rul+5, rul+2, rul+1, rul].map(v => Math.min(100, Math.max(0, v))); rulTrendChart.data.datasets[0].data = h; rulTrendChart.data.datasets[0].borderColor = clr; rulTrendChart.update(); } const rep = document.getElementById('ai-report-content'); if (rep) { rep.innerHTML = (typeof marked !== 'undefined' && marked.parse) ? marked.parse(d.report || '*No report generated.*') : (d.report || 'No report.'); } toast('Diagnostic complete', 'success'); } function show(id, display) { const el = document.getElementById(id); if (el) el.style.display = display || 'block'; } function hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; } // ─── 4. Reports ───────────────────────────────────────── async function loadReportsList() { const list = document.getElementById('reports-list'); if (!list) return; list.innerHTML = '
'; try { const res = await fetch(`${API}/reports`); if (!res.ok) throw new Error(); const data = await res.json(); state.reports = data.reports || []; if (!state.reports.length) { list.innerHTML = '
No saved reports.
'; return; } list.innerHTML = state.reports.map(r => `
Machine ${r.machine_id}
` ).join(''); } catch (e) { list.innerHTML = '
Error loading reports.
'; } } async function viewReport(file, el) { document.querySelectorAll('.report-item').forEach(i => i.classList.remove('active')); if (el) el.classList.add('active'); const v = document.getElementById('report-viewer'); if (!v) return; v.innerHTML = '
'; try { const res = await fetch(`${API}/report/${file}`); if (!res.ok) throw new Error(); const data = await res.json(); v.innerHTML = (typeof marked !== 'undefined' && marked.parse) ? marked.parse(data.content) : data.content; } catch (e) { v.innerHTML = '
Error loading report.
'; } } // ─── 5. Scheduling ────────────────────────────────────── function initSchedule() { const f = document.getElementById('schedule-filter'); if (f) f.addEventListener('change', renderSchedule); const modal = document.getElementById('reschedule-modal'); bindClick('btn-run-reschedule', () => { if (modal) modal.style.display = 'flex'; }); bindClick('modal-close', () => { if (modal) modal.style.display = 'none'; }); bindClick('modal-cancel', () => { if (modal) modal.style.display = 'none'; }); bindClick('modal-confirm', execReschedule); } function bindClick(id, fn) { const el = document.getElementById(id); if (el) el.addEventListener('click', fn); } async function loadSchedule() { try { const res = await fetch(`${API}/schedule`); if (!res.ok) throw new Error(); const data = await res.json(); if (data.error) { toast(data.error, 'info'); return; } state.schedule = data.data || []; updateSchedSummary(state.schedule); renderSchedule(); } catch (e) { toast('Error loading schedule', 'error'); } } function updateSchedSummary(s) { show('reschedule-summary', 'flex'); let moved = 0, alerts = 0; s.forEach(j => { if (j.status === 'Rescheduled') moved++; if (j.status === 'Risk Alert') alerts++; }); setText('rs-total', s.length); setText('rs-moved', moved); setText('rs-alerts', alerts); setText('rs-ok', s.length - moved - alerts); } function renderSchedule() { const tbody = document.getElementById('schedule-tbody'); if (!tbody) return; const f = valOf('schedule-filter', 'all'); let list = state.schedule; if (f !== 'all') list = list.filter(j => j.status === f); if (!list.length) { tbody.innerHTML = 'No jobs for this filter.'; return; } tbody.innerHTML = list.map(j => { const sc = j.status === 'Rescheduled' ? 'rescheduled' : j.status === 'Risk Alert' ? 'risk' : 'scheduled'; const pc = j.priority === 'High' ? 'high' : j.priority === 'Medium' ? 'medium' : 'low'; const dur = typeof j.duration_hours === 'number' ? j.duration_hours.toFixed(1) : j.duration_hours; return ` ${j.job_id} M-${j.machine_id} ${dur} h ${j.priority} ${j.status} ${j.notes||''} `; }).join(''); } async function execReschedule() { const btn = document.getElementById('modal-confirm'); if (!btn) return; const prev = btn.textContent; btn.textContent = 'Running...'; btn.disabled = true; try { const ids = (valOf('rs-machines', '')).split(',').map(s => s.trim()).filter(Boolean); const preds = {}; ids.forEach(id => { preds[id] = { failure_probability: 0.85, rul: 10.0 }; }); const res = await fetch(`${API}/reschedule`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ predictions: preds, failure_threshold: parseFloat(valOf('rs-threshold','0.7')), min_rul_hours: parseFloat(valOf('rs-min-rul','48')) }) }); if (!res.ok) throw new Error(); const data = await res.json(); state.schedule = data.schedule || []; updateSchedSummary(state.schedule); renderSchedule(); const modal = document.getElementById('reschedule-modal'); if (modal) modal.style.display = 'none'; toast(`Rescheduled ${data.rescheduled_count || 0} jobs`, 'success'); } catch (e) { toast('Rescheduling failed', 'error'); } finally { btn.textContent = prev; btn.disabled = false; } }