| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AERO|PULSE Dashboard</title> |
| |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Orbitron:wght@500;700&display=swap" rel="stylesheet"> |
| |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <style> |
| :root { |
| --bg-color: #0b1120; |
| --panel-bg: rgba(20, 30, 50, 0.6); |
| --border-color: rgba(60, 90, 130, 0.4); |
| --text-main: #e2e8f0; |
| --text-muted: #94a3b8; |
| --accent-blue: #38bdf8; |
| --accent-green: #34d399; |
| --accent-warning: #fbbf24; |
| --accent-danger: #f87171; |
| --glow: 0 0 15px rgba(56, 189, 248, 0.4); |
| } |
| |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| background-color: var(--bg-color); |
| color: var(--text-main); |
| display: flex; |
| height: 100vh; |
| overflow: hidden; |
| position: relative; |
| } |
| |
| .engine-bg { |
| position: fixed; |
| top: 0; |
| left: 250px; |
| width: calc(100vw - 250px); |
| height: 100vh; |
| background-image: url('engine_bg.png'); |
| background-size: cover; |
| background-position: top left; |
| z-index: -1; |
| opacity: 0.4; |
| mask-image: radial-gradient(circle at top left, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 65%); |
| -webkit-mask-image: radial-gradient(circle at top left, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 65%); |
| pointer-events: none; |
| } |
| |
| |
| .sidebar { |
| width: 240px; |
| background: rgba(10, 15, 30, 0.8); |
| border-right: 1px solid var(--border-color); |
| padding: 2rem 1rem; |
| display: flex; |
| flex-direction: column; |
| gap: 2rem; |
| backdrop-filter: blur(10px); |
| } |
| |
| .logo { |
| font-family: 'Orbitron', sans-serif; |
| font-size: 1.5rem; |
| font-weight: 700; |
| color: var(--text-main); |
| letter-spacing: 2px; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| .logo span { color: var(--accent-blue); text-shadow: var(--glow); } |
| |
| .nav-item { |
| padding: 0.8rem 1rem; |
| border-radius: 8px; |
| cursor: pointer; |
| color: var(--text-muted); |
| transition: all 0.3s ease; |
| font-weight: 600; |
| } |
| .nav-item:hover, .nav-item.active { |
| background: rgba(56, 189, 248, 0.1); |
| color: var(--accent-blue); |
| } |
| |
| |
| .main-content { |
| flex: 1; |
| padding: 2rem; |
| display: flex; |
| flex-direction: column; |
| gap: 1.5rem; |
| overflow-y: auto; |
| } |
| |
| .header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding-bottom: 1rem; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .status-badge { |
| display: inline-flex; |
| align-items: center; |
| gap: 8px; |
| background: rgba(52, 211, 153, 0.1); |
| color: var(--accent-green); |
| padding: 0.5rem 1rem; |
| border-radius: 20px; |
| font-size: 0.875rem; |
| font-weight: 600; |
| border: 1px solid rgba(52, 211, 153, 0.3); |
| } |
| .status-badge .dot { |
| width: 8px; height: 8px; |
| background: var(--accent-green); |
| border-radius: 50%; |
| box-shadow: 0 0 10px var(--accent-green); |
| } |
| |
| |
| .dashboard-grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| grid-template-rows: auto auto; |
| gap: 1.5rem; |
| height: 100%; |
| } |
| |
| .panel { |
| background: var(--panel-bg); |
| border: 1px solid var(--border-color); |
| border-radius: 16px; |
| padding: 1.5rem; |
| backdrop-filter: blur(12px); |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .panel-title { |
| font-size: 0.875rem; |
| color: var(--text-muted); |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| margin-bottom: 1rem; |
| font-weight: 600; |
| } |
| |
| |
| .rul-container { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| flex: 1; |
| } |
| |
| .gauge { |
| position: relative; |
| width: 250px; |
| height: 250px; |
| border-radius: 50%; |
| background: conic-gradient(var(--accent-warning) 0% 78%, rgba(255,255,255,0.05) 78% 100%); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| box-shadow: 0 0 30px rgba(251, 191, 36, 0.2); |
| margin-bottom: 1rem; |
| } |
| |
| .gauge::before { |
| content: ''; |
| position: absolute; |
| width: 210px; height: 210px; |
| background: var(--bg-color); |
| border-radius: 50%; |
| box-shadow: inset 0 0 20px rgba(0,0,0,0.8); |
| } |
| |
| .gauge-content { |
| position: relative; |
| z-index: 1; |
| text-align: center; |
| } |
| |
| .gauge-value { |
| font-size: 3.5rem; |
| font-family: 'Orbitron', sans-serif; |
| font-weight: 700; |
| background: linear-gradient(180deg, #fff, #fbbf24); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| line-height: 1.1; |
| } |
| |
| .gauge-label { color: var(--text-muted); font-size: 0.875rem; } |
| |
| .engine-selector { |
| background: rgba(10, 15, 30, 0.6); |
| color: #fff; |
| border: 1px solid var(--border-color); |
| padding: 0.85rem 1.25rem; |
| border-radius: 8px; |
| font-size: 1rem; |
| font-weight: 500; |
| margin-bottom: 0.75rem; |
| width: 100%; |
| cursor: pointer; |
| outline: none; |
| transition: all 0.3s ease; |
| appearance: none; |
| -webkit-appearance: none; |
| -moz-appearance: none; |
| background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2338bdf8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); |
| background-repeat: no-repeat; |
| background-position: right 1rem top 50%; |
| background-size: 0.8rem auto; |
| backdrop-filter: blur(10px); |
| box-shadow: inset 0 1px 3px rgba(0,0,0,0.3), 0 2px 4px rgba(0,0,0,0.1); |
| } |
| |
| .engine-selector:hover { |
| border-color: var(--accent-blue); |
| background-color: rgba(15, 25, 45, 0.8); |
| box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2); |
| transform: translateY(-1px); |
| } |
| |
| .engine-selector:focus { |
| border-color: var(--accent-blue); |
| } |
| |
| .engine-selector option { |
| background-color: #0f172a; |
| color: #fff; |
| } |
| |
| .engine-stats { |
| display: grid; |
| grid-template-columns: 1fr 1fr 1fr; |
| width: 100%; |
| gap: 1rem; |
| margin-top: 2rem; |
| border-top: 1px solid var(--border-color); |
| padding-top: 1rem; |
| text-align: center; |
| } |
| |
| .stat-val { font-size: 1.25rem; font-weight: 600; color: #fff; } |
| .stat-title { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; } |
| |
| |
| .chart-container { |
| position: relative; |
| flex: 1; |
| width: 100%; |
| min-height: 200px; |
| } |
| |
| .alerts-panel { |
| grid-row: span 2; |
| } |
| |
| .alert-item { |
| background: rgba(248, 113, 113, 0.1); |
| border-left: 4px solid var(--accent-danger); |
| padding: 1rem; |
| border-radius: 4px; |
| margin-bottom: 1rem; |
| } |
| .alert-title { color: var(--accent-danger); font-weight: 600; font-size: 0.875rem; } |
| .alert-desc { color: var(--text-muted); font-size: 0.75rem; margin-top: 4px; } |
| |
| |
| .view { display: none; height: 100%; } |
| .view.active { display: block; } |
| |
| |
| .fleet-table-container { |
| background: var(--panel-bg); |
| border: 1px solid var(--border-color); |
| border-radius: 16px; |
| padding: 1.5rem; |
| margin-top: 1.5rem; |
| overflow-y: auto; |
| max-height: calc(100vh - 120px); |
| } |
| |
| .fleet-table { |
| width: 100%; |
| border-collapse: collapse; |
| text-align: left; |
| } |
| |
| .fleet-table th { |
| color: var(--text-muted); |
| font-size: 0.75rem; |
| text-transform: uppercase; |
| padding: 1rem; |
| border-bottom: 1px solid var(--border-color); |
| position: sticky; |
| top: 0; |
| background: rgba(10, 15, 30, 0.95); |
| z-index: 2; |
| } |
| |
| .fleet-table td { |
| padding: 1rem; |
| border-bottom: 1px solid rgba(60, 90, 130, 0.2); |
| font-size: 0.875rem; |
| } |
| |
| .fleet-table tr:hover { |
| background: rgba(56, 189, 248, 0.05); |
| } |
| |
| .status-pill { |
| padding: 4px 12px; |
| border-radius: 20px; |
| font-size: 0.75rem; |
| font-weight: 600; |
| display: inline-block; |
| } |
| |
| </style> |
| </head> |
| <body> |
| <div class="engine-bg"></div> |
|
|
| <div class="sidebar"> |
| <div class="logo">AERO<span>|</span>PULSE</div> |
| <div class="nav-item active" onclick="switchView('dashboard', this)">Dashboard</div> |
| <div class="nav-item" onclick="switchView('analysis', this)">Analysis</div> |
| <div class="nav-item" onclick="switchView('engines', this)">Engines</div> |
| <div class="nav-item" onclick="switchView('maintenance', this)">Maintenance</div> |
| <div class="nav-item" onclick="switchView('alerts', this)">Alerts</div> |
| </div> |
|
|
| <div class="main-content"> |
| <div class="header"> |
| <div class="status-badge"><div class="dot"></div> System Status: Healthy</div> |
| <div style="color: var(--text-muted); font-size: 0.875rem;">LSTM Predictive Maintenance API</div> |
| </div> |
|
|
| <div id="view-dashboard" class="view active"> |
| <div class="dashboard-grid"> |
| |
| |
| <div class="panel"> |
| <div class="panel-title">Remaining Useful Life (RUL)</div> |
| <div class="rul-container"> |
| <div class="gauge" id="rulGauge"> |
| <div class="gauge-content"> |
| <div class="gauge-value" id="rulValue">--</div> |
| <div class="gauge-label">Cycles Remaining</div> |
| </div> |
| </div> |
| <div style="text-align: center; margin-bottom: 2rem; width: 90%; margin-left: auto; margin-right: auto;"> |
| <select id="engineSelect" class="engine-selector" onchange="changeEngine()"> |
| |
| </select> |
| <p style="color: var(--text-muted); font-size: 0.75rem; letter-spacing: 0.5px; text-transform: uppercase;">NASA CMAPSS Simulation</p> |
| </div> |
| <div class="engine-stats"> |
| <div> |
| <div class="stat-val" id="flightCycles">0</div> |
| <div class="stat-title">Flight Cycles</div> |
| </div> |
| <div> |
| <div class="stat-val" style="color: var(--accent-green);" id="engineStatus">OPERATIONAL</div> |
| <div class="stat-title">Status</div> |
| </div> |
| <div> |
| <button onclick="fetchEngineData()" style="background: var(--accent-blue); color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-weight: 600;">Refresh Data</button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="panel"> |
| <div class="panel-title">SENSOR TELEMETRY - Sensor 11 (EGT)</div> |
| <div class="chart-container"> |
| <canvas id="egtChart"></canvas> |
| </div> |
| </div> |
|
|
| |
| <div class="panel"> |
| <div class="panel-title">SENSOR TELEMETRY - Sensor 14 (Core Speed)</div> |
| <div class="chart-container"> |
| <canvas id="rpmChart"></canvas> |
| </div> |
| </div> |
|
|
| |
| <div class="panel alerts-panel"> |
| <div class="panel-title">Active Alerts</div> |
| <div id="alertsContainer"> |
| <p style="color: var(--text-muted); font-size: 0.875rem;">No critical alerts currently.</p> |
| </div> |
| </div> |
|
|
| </div> |
| </div> |
| |
| <div id="view-analysis" class="view"> |
| <h2 style="margin-bottom: 0.5rem; color: #fff;">Fleet Analysis & Diagnostics</h2> |
| <p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1.5rem;">Macro-level overview of RR Trent XWB predictive health models.</p> |
| |
| <div class="dashboard-grid"> |
| |
| <div class="panel" style="grid-column: span 2;"> |
| <div class="panel-title">Fleet RUL Distribution</div> |
| <div class="chart-container" style="height: 300px;"> |
| <canvas id="analysisChart"></canvas> |
| </div> |
| </div> |
|
|
| |
| <div class="panel"> |
| <div class="panel-title">Average Fleet RUL</div> |
| <div class="rul-container" style="margin-top: 1rem;"> |
| <div class="gauge" id="analysisGauge"> |
| <div class="gauge-content"> |
| <div class="gauge-value" id="analysisGaugeValue">--</div> |
| <div class="gauge-label">Avg. Cycles</div> |
| </div> |
| </div> |
| <div class="engine-stats" style="margin-top: 2rem;"> |
| <div> |
| <div class="stat-val" id="statCritical" style="color: var(--accent-danger);">0</div> |
| <div class="stat-title" style="font-size: 0.7rem;">Critical</div> |
| </div> |
| <div> |
| <div class="stat-val" id="statMaintenance" style="color: var(--accent-warning);">0</div> |
| <div class="stat-title" style="font-size: 0.7rem;">Maintenance</div> |
| </div> |
| <div> |
| <div class="stat-val" id="statOperational" style="color: var(--accent-green);">0</div> |
| <div class="stat-title" style="font-size: 0.7rem;">Operational</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div id="view-engines" class="view"> |
| <h2 style="margin-bottom: 0.5rem; color: #fff;">Fleet Overview</h2> |
| <p style="color: var(--text-muted); font-size: 0.875rem;">Real-time health status of all 100 RR Trent XWB engines</p> |
| |
| <div class="fleet-table-container"> |
| <table class="fleet-table"> |
| <thead> |
| <tr> |
| <th>Engine ID</th> |
| <th>Current Cycles</th> |
| <th>Predicted RUL</th> |
| <th>Health Status</th> |
| <th>Action</th> |
| </tr> |
| </thead> |
| <tbody id="fleetTableBody"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| <div id="view-maintenance" class="view"> |
| <h2 style="margin-bottom: 0.5rem; color: #fff;">Maintenance Work Orders</h2> |
| <p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1.5rem;">Manage and dispatch repair crews for degraded engines.</p> |
| |
| <div class="dashboard-grid"> |
| <div class="panel" style="grid-column: span 1;"> |
| <div class="panel-title" style="color: var(--accent-danger);">High Priority (Ground Immediate)</div> |
| <div id="maintenanceCriticalList" style="margin-top: 1rem;"> |
| |
| </div> |
| </div> |
| <div class="panel" style="grid-column: span 1;"> |
| <div class="panel-title" style="color: var(--accent-warning);">Scheduled Maintenance (Due Soon)</div> |
| <div id="maintenanceWarningList" style="margin-top: 1rem;"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div id="view-alerts" class="view"> |
| <h2 style="margin-bottom: 0.5rem; color: #fff;">System Alerts Log</h2> |
| <p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1.5rem;">Chronological history of all automated RUL warnings.</p> |
| |
| <div class="fleet-table-container" style="background: transparent; border: none; padding: 0; margin-top: 0;"> |
| <div id="alertsLogContainer" style="display: flex; flex-direction: column; gap: 1rem;"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| <script> |
| const chartOptions = { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| legend: { display: false } |
| }, |
| scales: { |
| x: { display: false }, |
| y: { |
| grid: { color: 'rgba(60, 90, 130, 0.05)' }, |
| border: { display: false }, |
| ticks: { color: '#94a3b8' } |
| } |
| }, |
| elements: { |
| point: { radius: 0 } |
| } |
| }; |
| |
| function createChart(ctx, color, label) { |
| |
| let gradient = ctx.createLinearGradient(0, 0, 0, 300); |
| gradient.addColorStop(0, color + '22'); |
| gradient.addColorStop(1, color + '00'); |
| |
| return new Chart(ctx, { |
| type: 'line', |
| data: { |
| labels: [], |
| datasets: [{ |
| label: label, |
| borderColor: color, |
| backgroundColor: gradient, |
| borderWidth: 2, |
| tension: 0.4, |
| fill: true |
| }] |
| }, |
| options: chartOptions |
| }); |
| } |
| |
| |
| const egtChart = createChart(document.getElementById('egtChart').getContext('2d'), '#f97316', 'Temperature'); |
| const rpmChart = createChart(document.getElementById('rpmChart').getContext('2d'), '#38bdf8', 'RPM'); |
| |
| let analysisChart = new Chart(document.getElementById('analysisChart'), { |
| type: 'bar', |
| data: { |
| labels: ['Critical (<20)', 'Maintenance Due (20-50)', 'Operational (>50)'], |
| datasets: [{ |
| label: 'Number of Engines', |
| data: [0, 0, 0], |
| backgroundColor: [ |
| 'rgba(248, 113, 113, 0.8)', |
| 'rgba(251, 191, 36, 0.8)', |
| 'rgba(52, 211, 153, 0.8)' |
| ], |
| borderRadius: 4 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { legend: { display: false } }, |
| scales: { |
| y: { beginAtZero: true, grid: { color: 'rgba(60, 90, 130, 0.1)' } }, |
| x: { grid: { display: false } } |
| } |
| } |
| }); |
| |
| let currentCycle = 0; |
| let currentEngineId = 1; |
| |
| |
| const engineSelect = document.getElementById('engineSelect'); |
| for(let i=1; i<=100; i++) { |
| const opt = document.createElement('option'); |
| opt.value = i; |
| opt.innerText = `Engine #${i} - RR Trent XWB`; |
| engineSelect.appendChild(opt); |
| } |
| |
| function changeEngine() { |
| currentEngineId = document.getElementById('engineSelect').value; |
| currentCycle = 0; |
| document.getElementById('flightCycles').innerText = currentCycle; |
| document.getElementById('rulValue').innerText = '--'; |
| document.getElementById('engineStatus').innerText = 'OPERATIONAL'; |
| document.getElementById('engineStatus').style.color = '#34d399'; |
| |
| |
| [egtChart, rpmChart].forEach(chart => { |
| chart.data.datasets[0].data = []; |
| chart.update(); |
| }); |
| |
| fetchEngineData(); |
| } |
| |
| |
| async function fetchEngineData() { |
| try { |
| const response = await fetch(`/engine/${currentEngineId}/history`); |
| |
| if (response.ok) { |
| const data = await response.json(); |
| |
| document.getElementById('flightCycles').innerText = data.current_cycle; |
| updateDashboard(data.predicted_rul); |
| |
| |
| egtChart.data.labels = data.history.cycles; |
| egtChart.data.datasets[0].data = data.history.sensor_11; |
| egtChart.update(); |
| |
| rpmChart.data.labels = data.history.cycles; |
| rpmChart.data.datasets[0].data = data.history.sensor_14; |
| rpmChart.update(); |
| } else { |
| console.error("Failed to load engine data"); |
| } |
| } catch (e) { |
| console.error("Backend offline", e); |
| } |
| } |
| |
| function updateDashboard(rul) { |
| const rulValue = document.getElementById('rulValue'); |
| const gauge = document.getElementById('rulGauge'); |
| const status = document.getElementById('engineStatus'); |
| const alerts = document.getElementById('alertsContainer'); |
| |
| rul = Math.round(rul); |
| rulValue.innerText = rul; |
| |
| |
| let pct = Math.min((rul / 200) * 100, 100); |
| |
| |
| let color = '#34d399'; |
| if (rul < 50) color = '#fbbf24'; |
| if (rul < 20) color = '#f87171'; |
| |
| gauge.style.background = `conic-gradient(${color} 0% ${pct}%, rgba(255,255,255,0.05) ${pct}% 100%)`; |
| rulValue.style.background = `linear-gradient(180deg, #fff, ${color})`; |
| rulValue.style.webkitBackgroundClip = 'text'; |
| |
| if (rul >= 50) { |
| status.innerText = 'OPERATIONAL'; |
| status.style.color = '#34d399'; |
| alerts.innerHTML = '<p style="color: var(--text-muted); font-size: 0.875rem;">No critical alerts currently.</p>'; |
| } else if (rul >= 20) { |
| status.innerText = 'MAINTENANCE DUE'; |
| status.style.color = '#fbbf24'; |
| alerts.innerHTML = ` |
| <div class="alert-item" style="border-left-color: #fbbf24; background: rgba(251, 191, 36, 0.1);"> |
| <div class="alert-title" style="color: #fbbf24;">Maintenance Warning</div> |
| <div class="alert-desc">RUL has dropped below 50 cycles. Schedule inspection.</div> |
| </div>`; |
| } else { |
| status.innerText = 'CRITICAL FAILING'; |
| status.style.color = '#f87171'; |
| alerts.innerHTML = ` |
| <div class="alert-item"> |
| <div class="alert-title">CRITICAL ALERT</div> |
| <div class="alert-desc">Engine failure imminent. RUL < 20 cycles. Ground aircraft immediately.</div> |
| </div>`; |
| } |
| |
| |
| } |
| |
| |
| function switchView(viewId, element) { |
| document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); |
| document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); |
| |
| document.getElementById('view-' + viewId).classList.add('active'); |
| element.classList.add('active'); |
| |
| if (viewId === 'engines') { |
| generateFleetTable(); |
| } else if (viewId === 'analysis') { |
| generateAnalysisView(); |
| } else if (viewId === 'maintenance') { |
| generateMaintenanceView(); |
| } else if (viewId === 'alerts') { |
| generateAlertsView(); |
| } |
| } |
| |
| async function generateAnalysisView() { |
| try { |
| const response = await fetch('/fleet'); |
| if (response.ok) { |
| const engines = await response.json(); |
| |
| let critical = 0; |
| let warning = 0; |
| let operational = 0; |
| let totalRul = 0; |
| |
| engines.forEach(eng => { |
| let rul = Math.round(eng.rul); |
| totalRul += rul; |
| if (rul < 20) critical++; |
| else if (rul <= 50) warning++; |
| else operational++; |
| }); |
| |
| |
| document.getElementById('statCritical').innerText = critical; |
| document.getElementById('statMaintenance').innerText = warning; |
| document.getElementById('statOperational').innerText = operational; |
| |
| |
| let avgRul = Math.round(totalRul / engines.length); |
| document.getElementById('analysisGaugeValue').innerText = avgRul; |
| |
| const gauge = document.getElementById('analysisGauge'); |
| if (avgRul > 50) { |
| gauge.style.background = `conic-gradient(var(--accent-green) ${(avgRul/150)*100}%, rgba(255,255,255,0.05) 0)`; |
| document.getElementById('analysisGaugeValue').style.color = 'var(--accent-green)'; |
| } else if (avgRul > 20) { |
| gauge.style.background = `conic-gradient(var(--accent-warning) ${(avgRul/150)*100}%, rgba(255,255,255,0.05) 0)`; |
| document.getElementById('analysisGaugeValue').style.color = 'var(--accent-warning)'; |
| } else { |
| gauge.style.background = `conic-gradient(var(--accent-danger) ${(avgRul/150)*100}%, rgba(255,255,255,0.05) 0)`; |
| document.getElementById('analysisGaugeValue').style.color = 'var(--accent-danger)'; |
| } |
| |
| |
| analysisChart.data.datasets[0].data = [critical, warning, operational]; |
| analysisChart.update(); |
| } |
| } catch (e) { |
| console.error("Failed to load analysis data", e); |
| } |
| } |
| |
| async function generateFleetTable() { |
| const tbody = document.getElementById('fleetTableBody'); |
| tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: var(--text-muted); padding: 2rem;">Analyzing all 100 engines...</td></tr>'; |
| |
| try { |
| const response = await fetch('/fleet'); |
| if (response.ok) { |
| const engines = await response.json(); |
| tbody.innerHTML = ''; |
| |
| engines.forEach(eng => { |
| let statusHtml = ''; |
| let rul = Math.round(eng.rul); |
| |
| if (rul >= 50) { |
| statusHtml = `<span class="status-pill" style="background: rgba(52, 211, 153, 0.1); color: var(--accent-green); border: 1px solid var(--accent-green);">OPERATIONAL</span>`; |
| } else if (rul >= 20) { |
| statusHtml = `<span class="status-pill" style="background: rgba(251, 191, 36, 0.1); color: var(--accent-warning); border: 1px solid var(--accent-warning);">MAINTENANCE DUE</span>`; |
| } else { |
| statusHtml = `<span class="status-pill" style="background: rgba(248, 113, 113, 0.1); color: var(--accent-danger); border: 1px solid var(--accent-danger);">CRITICAL FAILING</span>`; |
| } |
| |
| const tr = document.createElement('tr'); |
| tr.innerHTML = ` |
| <td style="font-weight: 600;">Engine #${eng.id}</td> |
| <td>${eng.cycles}</td> |
| <td style="font-family: 'Orbitron', sans-serif;">${rul} cycles</td> |
| <td>${statusHtml}</td> |
| <td> |
| <button onclick="inspectEngine(${eng.id})" style="background: transparent; color: var(--accent-blue); border: 1px solid var(--accent-blue); padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 0.75rem;">Inspect</button> |
| </td> |
| `; |
| tbody.appendChild(tr); |
| }); |
| } |
| } catch (e) { |
| console.error("Failed to load fleet data", e); |
| } |
| } |
| |
| function inspectEngine(id) { |
| |
| document.getElementById('engineSelect').value = id; |
| changeEngine(); |
| switchView('dashboard', document.querySelector('.nav-item')); |
| } |
| |
| async function generateMaintenanceView() { |
| document.getElementById('maintenanceCriticalList').innerHTML = '<p style="color: var(--text-muted);">Loading...</p>'; |
| document.getElementById('maintenanceWarningList').innerHTML = '<p style="color: var(--text-muted);">Loading...</p>'; |
| |
| try { |
| const response = await fetch('/fleet'); |
| if (response.ok) { |
| const engines = await response.json(); |
| let critHtml = ''; |
| let warnHtml = ''; |
| |
| engines.forEach(eng => { |
| let rul = Math.round(eng.rul); |
| if (rul < 20) { |
| critHtml += ` |
| <div style="background: rgba(248, 113, 113, 0.05); border: 1px solid var(--accent-danger); border-radius: 8px; padding: 1rem; margin-bottom: 1rem;"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;"> |
| <strong style="color: var(--accent-danger); font-size: 1.1rem;">Engine #${eng.id}</strong> |
| <span style="font-family: 'Orbitron', sans-serif; font-size: 0.875rem; color: #fff;">RUL: ${rul}</span> |
| </div> |
| <p style="font-size: 0.75rem; color: var(--text-muted); margin-bottom: 1rem;">Requires immediate HPC inspection. High risk of failure.</p> |
| <button style="background: var(--accent-danger); color: #fff; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-weight: 600;">Dispatch Crew</button> |
| </div> |
| `; |
| } else if (rul <= 50) { |
| warnHtml += ` |
| <div style="background: rgba(251, 191, 36, 0.05); border: 1px solid rgba(251, 191, 36, 0.3); border-radius: 8px; padding: 1rem; margin-bottom: 1rem;"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;"> |
| <strong style="color: var(--accent-warning); font-size: 1.1rem;">Engine #${eng.id}</strong> |
| <span style="font-family: 'Orbitron', sans-serif; font-size: 0.875rem; color: #fff;">RUL: ${rul}</span> |
| </div> |
| <p style="font-size: 0.75rem; color: var(--text-muted); margin-bottom: 1rem;">Schedule routine maintenance within ${rul} flights.</p> |
| <button style="background: transparent; color: var(--accent-warning); border: 1px solid var(--accent-warning); padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-weight: 600;">Schedule Task</button> |
| </div> |
| `; |
| } |
| }); |
| |
| document.getElementById('maintenanceCriticalList').innerHTML = critHtml || '<p style="color: var(--text-muted); font-size: 0.875rem;">No critical work orders.</p>'; |
| document.getElementById('maintenanceWarningList').innerHTML = warnHtml || '<p style="color: var(--text-muted); font-size: 0.875rem;">No scheduled maintenance.</p>'; |
| } |
| } catch (e) { |
| console.error("Failed to load maintenance data", e); |
| } |
| } |
| |
| async function generateAlertsView() { |
| const container = document.getElementById('alertsLogContainer'); |
| container.innerHTML = '<p style="color: var(--text-muted);">Loading system logs...</p>'; |
| |
| try { |
| const response = await fetch('/fleet'); |
| if (response.ok) { |
| const engines = await response.json(); |
| let logHtml = ''; |
| |
| |
| engines.sort((a, b) => a.rul - b.rul); |
| |
| engines.forEach(eng => { |
| let rul = Math.round(eng.rul); |
| let timeOffset = Math.floor(Math.random() * 60); |
| |
| if (rul < 20) { |
| logHtml += ` |
| <div style="background: var(--panel-bg); border-left: 4px solid var(--accent-danger); padding: 1.25rem; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> |
| <div> |
| <div style="color: var(--accent-danger); font-weight: 600; margin-bottom: 4px;">CRITICAL: Engine #${eng.id} Remaining Useful Life breached 20 cycles</div> |
| <div style="color: var(--text-muted); font-size: 0.875rem;">Automated grounding protocol recommended. Estimated RUL: <span style="color:#fff; font-weight:600">${rul}</span> cycles.</div> |
| </div> |
| <div style="color: var(--text-muted); font-size: 0.75rem; min-width: 80px; text-align: right;">${timeOffset} mins ago</div> |
| </div> |
| `; |
| } else if (rul <= 50) { |
| logHtml += ` |
| <div style="background: var(--panel-bg); border-left: 4px solid var(--accent-warning); padding: 1.25rem; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> |
| <div> |
| <div style="color: var(--accent-warning); font-weight: 600; margin-bottom: 4px;">WARNING: Engine #${eng.id} degraded below operational threshold</div> |
| <div style="color: var(--text-muted); font-size: 0.875rem;">Maintenance scheduling required. Estimated RUL: <span style="color:#fff; font-weight:600">${rul}</span> cycles.</div> |
| </div> |
| <div style="color: var(--text-muted); font-size: 0.75rem; min-width: 80px; text-align: right;">${timeOffset + 60} mins ago</div> |
| </div> |
| `; |
| } |
| }); |
| |
| container.innerHTML = logHtml || '<p style="color: var(--text-muted);">No system alerts. Fleet is healthy.</p>'; |
| } |
| } catch (e) { |
| console.error("Failed to load alerts data", e); |
| } |
| } |
| |
| |
| window.onload = () => { |
| fetchEngineData(); |
| }; |
| </script> |
| </body> |
| </html> |
|
|