SyedaArisha's picture
Upload folder using huggingface_hub
baf834b verified
Raw History Blame Contribute Delete
19.4 kB
/**
* 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 = '<div class="empty-msg">Unable to connect to backend.</div>';
}
}
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 = '<div class="empty-msg">No machines match filters.</div>'; return; }
grid.innerHTML = list.map(m => {
const s = m.status === 'Critical' ? 'crit' : m.status === 'At Risk' ? 'warn' : 'ok';
return `<div class="machine-card${s==='crit' ? ' machine-card--critical':''}">
<div class="mc-head">
<div><span class="mc-id">M-${m.machine_id}</span> <span class="mc-model">${m.model}</span></div>
<span class="mc-status mc-status--${s}"><span class="mc-dot mc-dot--${s}"></span>${m.status}</span>
</div>
<div class="mc-prob">Failure risk <strong>${(m.failure_probability*100).toFixed(1)}%</strong></div>
<div class="mc-rul"><span class="mc-rul-val">${m.rul_hours.toFixed(1)}</span><span class="mc-rul-unit">h</span></div>
<div class="mc-sensors"><div>V ${m.sensors.volt}</div><div>R ${m.sensors.rotate}</div><div>P ${m.sensors.pressure}</div><div>Vib ${m.sensors.vibration}</div></div>
</div>`;
}).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 = '<div class="empty-msg"><div class="loader-bar" style="width:40px;margin:0 auto"></div></div>';
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 = '<div class="empty-msg">No saved reports.</div>'; return; }
list.innerHTML = state.reports.map(r =>
`<div class="report-item" onclick="viewReport('${r.filename}', this)">Machine ${r.machine_id}</div>`
).join('');
} catch (e) { list.innerHTML = '<div class="empty-msg">Error loading reports.</div>'; }
}
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 = '<div class="empty-msg"><div class="loader-bar" style="width:40px;margin:0 auto"></div></div>';
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 = '<div class="empty-msg">Error loading report.</div>'; }
}
// ─── 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 = '<tr><td colspan="6" class="tbl-empty">No jobs for this filter.</td></tr>'; 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 `<tr>
<td class="tbl-mono">${j.job_id}</td>
<td class="tbl-mono tbl-bold">M-${j.machine_id}</td>
<td>${dur} h</td>
<td class="tbl-prio tbl-prio--${pc}">${j.priority}</td>
<td class="tbl-status tbl-status--${sc}">${j.status}</td>
<td style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${j.notes||''}">${j.notes||''}</td>
</tr>`;
}).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; }
}