Spaces:
Running
Running
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>GridPulse Edge – Energy-Efficient Grid Monitor</title> | |
| <!-- Tailwind + Feather + Chart.js + date-fns --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script> | |
| <style> | |
| :root { | |
| --primary: #14b8a6; | |
| --secondary: #f59e0b; | |
| --dark: #111827; | |
| --light: #fdfdfd; | |
| } | |
| .theme-light { --bg: var(--light); --text: var(--dark); } | |
| .theme-dark { --bg: var(--dark); --text: var(--light); } | |
| body { background: var(--bg); color: var(--text); transition: background .3s, color .3s; } | |
| .toast-enter { opacity:0; transform:translateY(-20px); } | |
| .toast-enter-active { opacity:1; transform:translateY(0); transition:all .4s; } | |
| .blink { animation:blink 1s infinite; } | |
| @keyframes blink{ 50%{ filter:brightness(1.4) drop-shadow(0 0 8px var(--secondary));} } | |
| </style> | |
| </head> | |
| <body class="theme-dark font-sans"> | |
| <!-- TOAST --> | |
| <div id="toast" class="fixed top-4 right-4 z-50 max-w-sm"></div> | |
| <!-- HEADER --> | |
| <header class="flex flex-col sm:flex-row justify-between items-center p-4 md:p-6 bg-slate-900"> | |
| <h1 class="text-xl md:text-3xl font-extrabold flex items-center gap-3"> | |
| <i data-feather="bar-chart-2" class="text-emerald-500"></i> | |
| GridPulse Edge | |
| </h1> | |
| <div class="flex items-center gap-3 mt-2 sm:mt-0"> | |
| <button id="themeBtn" class="p-2 rounded-full bg-slate-700 hover:bg-slate-600"> | |
| <i data-feather="sun"></i> | |
| </button> | |
| <select id="rangeSelect" class="bg-slate-700 text-sm rounded p-2"> | |
| <option value="15m">Last 15 min</option> | |
| <option value="1h">1 hour</option> | |
| <option value="24h">24 hours</option> | |
| </select> | |
| </div> | |
| </header> | |
| <!-- KPI ROW --> | |
| <section class="grid grid-cols-2 lg:grid-cols-5 gap-4 p-4 md:p-6"> | |
| <div id="kpiHealth" | |
| class="col-span-2 lg:col-span-1 flex flex-col justify-center items-center bg-slate-800 rounded-2xl p-4 min-h-[140px]"> | |
| <h3 class="text-sm opacity-60 mb-1">Health</h3> | |
| <div id="gauge" class="w-16 h-16 rounded-full border-8 border-emerald-500" style="--tw-border-opacity:.7;"></div> | |
| <span id="healthText" class="mt-2 font-bold text-xl">0.94</span> | |
| </div> | |
| <div id="kpiEta" | |
| class="flex justify-center items-center text-center bg-slate-800 rounded-2xl p-4"> | |
| <div> | |
| <h3 class="text-sm opacity-60">Next Fault ETA</h3> | |
| <span id="etaVal" class="font-bold text-2xl">27 s</span> | |
| </div> | |
| </div> | |
| <div id="kpiFault" | |
| class="flex justify-center items-center text-center bg-slate-800 rounded-2xl p-4"> | |
| <div> | |
| <h3 class="text-sm opacity-60">Top Fault</h3> | |
| <span id="faultVal" class="font-bold text-lg">Sag 0.82</span> | |
| </div> | |
| </div> | |
| <div id="kpiLatency" | |
| class="flex justify-center items-center text-center bg-slate-800 rounded-2xl p-4"> | |
| <h3 class="text-sm opacity-60">Latency / Energy</h3> | |
| <div class="font-bold text-lg"> | |
| <span id="latencyVal">0.12 ms</span><br/> | |
| <span id="energyVal">0.004 Wh</span> | |
| </div> | |
| </div> | |
| <div id="kpiCO2" | |
| class="flex justify-center items-center text-center bg-slate-800 rounded-2xl p-4"> | |
| <div> | |
| <h3 class="text-sm opacity-60">CO₂ avoided today</h3> | |
| <span id="co2Val" class="font-bold text-xl">2.1 kg</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- MAIN GRID --> | |
| <main class="grid grid-cols-1 xl:grid-cols-2 gap-6 px-4 md:px-6 pb-6"> | |
| <!-- CHART 1: HEALTH --> | |
| <section class="bg-slate-800 rounded-2xl p-4"> | |
| <h2 class="mb-2 font-bold">Health score – 15 min</h2> | |
| <canvas id="healthChart" height="120"></canvas> | |
| </section> | |
| <!-- CHART 2: PROBABILITIES --> | |
| <section class="bg-slate-800 rounded-2xl p-4"> | |
| <h2 class="mb-2 font-bold">Fault probabilities</h2> | |
| <canvas id="probChart" height="120"></canvas> | |
| </section> | |
| <!-- CHART 3: ENERGY LEDGER --> | |
| <section class="bg-slate-800 rounded-2xl p-4"> | |
| <h2 class="mb-2 font-bold">Energy ledger – today</h2> | |
| <canvas id="energyChart" height="120"></canvas> | |
| </section> | |
| <!-- CHART 4: LATENCY / COST --> | |
| <section class="bg-slate-800 rounded-2xl p-4"> | |
| <h2 class="mb-2 font-bold">Latency & cost</h2> | |
| <canvas id="latencyChart" height="120"></canvas> | |
| </section> | |
| </main> | |
| <!-- TABLES --> | |
| <section class="px-4 md:px-6 pb-6 grid grid-cols-1 xl:grid-cols-2 gap-6"> | |
| <!-- ALERTS --> | |
| <section class="bg-slate-800 rounded-2xl p-4"> | |
| <h2 class="mb-3 font-bold">Recent alerts (last 24 h)</h2> | |
| <div class="overflow-x-auto"> | |
| <table id="alertsTable" class="w-full text-sm"> | |
| <thead> | |
| <tr class="border-b border-slate-600"> | |
| <th class="p-2 text-left">Time</th><th>Feeder</th><th>Fault</th><th>ETA</th><th>Action</th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </section> | |
| <!-- MODEL COMPARISON --> | |
| <section class="bg-slate-800 rounded-2xl p-4"> | |
| <h2 class="mb-3 font-bold">Model comparison</h2> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full text-sm"> | |
| <thead> | |
| <tr class="border-b border-slate-600"> | |
| <th class="p-2 text-left">Model</th><th>Latency(ms)</th><th>Energy(Wh)</th><th>Cost($)</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr class="bg-emerald-700/30"><td class="p-2 font-bold">LR INT8 *</td><td>0.15</td><td>0.005</td><td>0.000064</td></tr> | |
| <tr><td class="p-2">RF</td><td>0.8</td><td>0.027</td><td>0.00035</td></tr> | |
| <tr><td class="p-2">XGBoost INT8</td><td>0.22</td><td>0.0081</td><td>0.00011</td></tr> | |
| <tr><td class="p-2">LSTM FP16</td><td>2.1</td><td>0.065</td><td>0.00084</td></tr> | |
| <tr><td class="p-2">GNN INT8</td><td>1.5</td><td>0.042</td><td>0.00055</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </section> | |
| </section> | |
| <footer class="text-center text-xs opacity-60 p-4"> | |
| Energy, latency, and cost figures adapted from literature; refined with deployment telemetry. | |
| </footer> | |
| <script> | |
| /* UTILS */ | |
| const dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; | |
| const body = document.body; | |
| function setTheme(darkMode){ | |
| body.className = darkMode ? 'theme-dark' : 'theme-light'; | |
| document.querySelector('#themeBtn').innerHTML = feather.icons[darkMode?'sun':'moon'].toSvg({class:'inline w-5'}); | |
| } | |
| setTheme(dark); | |
| document.getElementById('themeBtn').onclick = ()=>setTheme(!body.classList.contains('theme-dark')); | |
| /* DEMO DATA GENERATION */ | |
| let lastHealth=0.94, lastEta=180; | |
| // Template data for Health Score (15 minutes, updated every second) | |
| const dataHealth = []; | |
| const now = Date.now(); | |
| for (let i = 0; i < 15 * 60; i++) { | |
| const time = now - (15 * 60 - i) * 1000; | |
| // Use template values that vary realistically | |
| const values = [0.95, 0.94, 0.93, 0.92, 0.91, 0.90, 0.89, 0.88, 0.87, 0.86]; | |
| const value = values[i % values.length] + (Math.random() * 0.02 - 0.01); | |
| dataHealth.push({ x: time, y: Math.max(0.85, Math.min(0.96, value)) }); | |
| } | |
| // Template data for Fault Probabilities (24 hours, updated every minute) | |
| const dataSag = [], dataSwell = [], dataOsc = []; | |
| for (let i = 0; i < 24 * 60; i++) { | |
| const time = now - (24 * 60 - i) * 60000; | |
| // Use template patterns for each fault type | |
| dataSag.push({ x: time, y: 0.1 + 0.1 * Math.sin(i / 30) + Math.random() * 0.05 }); | |
| dataSwell.push({ x: time, y: 0.05 + 0.08 * Math.sin(i / 45 + 1) + Math.random() * 0.03 }); | |
| dataOsc.push({ x: time, y: 0.02 + 0.06 * Math.sin(i / 60 + 2) + Math.random() * 0.02 }); | |
| } | |
| // Template data for Energy Ledger (24 hours, hourly data) | |
| const dataEnergy = []; | |
| for (let i = 0; i < 24; i++) { | |
| const time = now - (23 - i) * 3600000; | |
| // Template energy consumption pattern | |
| const base = 0.005 + 0.003 * Math.sin(i / 4); | |
| dataEnergy.push({ x: time, y: base + Math.random() * 0.002 }); | |
| } | |
| // Template data for Latency & Cost (24 hours, hourly data) | |
| const dataLatency = [], dataCost = []; | |
| for (let i = 0; i < 24; i++) { | |
| const time = now - (23 - i) * 3600000; | |
| // Template latency pattern | |
| const latency = 0.1 + 0.05 * Math.sin(i / 3) + Math.random() * 0.02; | |
| dataLatency.push({ x: time, y: latency }); | |
| // Cost derived from latency | |
| dataCost.push({ x: time, y: latency * 0.0005 }); | |
| } | |
| // Template data for Recent Alerts | |
| const alerts = [ | |
| {time:new Date(now-2*60000), feeder:"F01", fault:"Voltage Sag 0.85", eta:"32 s", action:"Adjust tap changer"}, | |
| {time:new Date(now-5*60000), feeder:"F02", fault:"Current Swell 0.78", eta:"1:45", action:"Notify maintenance"}, | |
| {time:new Date(now-10*60000), feeder:"F03", fault:"Frequency Osc 0.65", eta:"2:10", action:"Monitor"}, | |
| {time:new Date(now-30*60000), feeder:"F04", fault:"Harmonic Dist 0.72", eta:"4:30", action:"Schedule filter check"}, | |
| {time:new Date(now-60*60000), feeder:"F05", fault:"Voltage Dip 0.88", eta:"12:30", action:"Reroute load"}, | |
| {time:new Date(now-90*60000), feeder:"F06", fault:"Phase Unbalance 0.81", eta:"6:15", action:"Balance load"}, | |
| {time:new Date(now-120*60000), feeder:"F07", fault:"Capacitor Bank Fail", eta:"8:20", action:"Dispatch crew"} | |
| ]; | |
| /* CHARTS */ | |
| function lineConfig(data,label,color,fill=false){ | |
| return { | |
| type:"line", data:{datasets:[{label,data,backgroundColor:fill?color+'33':'transparent',borderColor:color,fill}]}, | |
| options:{animation:false, plugins:{legend:{display:false}}, scales:{x:{type:'time',ticks:{maxTicksLimit:5}}}} | |
| }; | |
| } | |
| window.onload = ()=>{ | |
| new Chart(document.getElementById('healthChart'), lineConfig(dataHealth.slice(-900),'Health','#10b981',true)); | |
| new Chart(document.getElementById('probChart'), { | |
| type:'line', | |
| data:{datasets:[ | |
| {label:'Sag',data:dataSag,backgroundColor:'transparent',borderColor:'#ef4444',pointRadius:0}, | |
| {label:'Swell',data:dataSwell,backgroundColor:'transparent',borderColor:'#f59e0b',pointRadius:0}, | |
| {label:'Oscillation',data:dataOsc,backgroundColor:'transparent',borderColor:'#3b82f6',pointRadius:0} | |
| ]}, | |
| options:{animation:false, plugins:{legend:{display:false}}, scales:{x:{type:'time',ticks:{maxTicksLimit:5}}}} | |
| }); | |
| new Chart(document.getElementById('energyChart'), { | |
| type:'bar', | |
| data:{datasets:[ | |
| {label:'Model energy',data:dataEnergy.slice(-24),backgroundColor:'#6366f1'}, | |
| {label:'Energy saved',data:Array(24).fill(0.025),backgroundColor:'#34d399'} | |
| ]}, | |
| options:{animation:false, plugins:{legend:{display:false}}, scales:{x:{ticks:{maxTicksLimit:12}}}} | |
| }); | |
| new Chart(document.getElementById('latencyChart'), { | |
| type:'line', | |
| data:{datasets:[ | |
| {label:'Latency ms',data:dataLatency.slice(-24),backgroundColor:'transparent',borderColor:'#8b5cf6',yAxisID:'y'}, | |
| {label:'Cost $',data:dataCost.slice(-24),backgroundColor:'transparent',borderColor:'#ec4899',borderDash:[5,5],yAxisID:'y1'} | |
| ]}, | |
| options:{animation:false, interaction:{mode:'nearest', axis:'x', intersect:false}, scales:{y:{type:'linear',position:'left'}, y1:{type:'linear',position:'right',grid:{drawOnChartArea:false}}}} | |
| }); | |
| }; | |
| /* REFRESH UI */ | |
| function refresh(){ | |
| lastHealth = Math.max(0,lastHealth-0.001+Math.random()*0.002); | |
| document.getElementById('healthText').textContent = lastHealth.toFixed(2); | |
| const gauge = document.getElementById('gauge'); | |
| let c = lastHealth>=0.8? '#22c55e':lastHealth>=0.5? '#f59e0b': '#ef4444'; | |
| gauge.style.borderColor = c; | |
| lastEta = Math.max(0,lastEta-1+Math.random()*2); | |
| const eta = lastEta<60?Math.round(lastEta)+' s':`${Math.floor(lastEta/60)} m`; | |
| document.getElementById('etaVal').textContent=eta; | |
| const kpiEta = document.getElementById('kpiEta'); | |
| kpiEta.classList.toggle('blink', lastEta<60); | |
| const hour = new Date().getHours(); | |
| const isPeak = hour > 8 && hour < 20; | |
| // Use template values for latency and energy | |
| const latencyVal = isPeak ? 0.15 + Math.random() * 0.03 : 0.12 + Math.random() * 0.02; | |
| const energyVal = isPeak ? 0.008 + Math.random() * 0.0015 : 0.006 + Math.random() * 0.0012; | |
| document.getElementById('latencyVal').textContent = latencyVal.toFixed(2) + ' ms'; | |
| document.getElementById('energyVal').textContent = energyVal.toFixed(3) + ' Wh'; | |
| document.getElementById('co2Val').textContent = (2.1 + Math.random() * 0.2).toFixed(1) + ' kg'; | |
| // alerts | |
| const tbody = document.querySelector('#alertsTable tbody'); | |
| tbody.innerHTML=''; | |
| alerts.forEach(a=>{ | |
| const tr=document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td class="p-2">${format(a.time,'HH:mm:ss')}</td> | |
| <td>${a.feeder}</td> | |
| <td>${a.fault}</td> | |
| <td>${a.eta}</td> | |
| <td class="italic">${a.action}</td>`; | |
| tbody.append(tr); | |
| }); | |
| if(lastEta < 30) showToast(`Critical fault predicted in ${lastEta}s`, {type:'danger'}); | |
| else if(Math.random()>0.997) showToast('Load imbalance detected in Sector N', {type:'warning'}); | |
| } | |
| const format = dateFns.format; | |
| setInterval(refresh,1000); | |
| /* TOAST MANAGER */ | |
| function showToast(msg, opts={}){ | |
| const box=document.createElement('div'); | |
| box.className='toast-enter bg-red-600 text-white p-3 rounded shadow'; | |
| box.textContent = msg; | |
| document.getElementById('toast').appendChild(box); | |
| box.classList.add('toast-enter-active'); | |
| setTimeout(()=>box.remove(),4000); | |
| } | |
| refresh(); | |
| feather.replace(); | |
| </script> | |
| </body> | |
| </html> | |