gridpulse-pulsemaster / index.html
Yaroster's picture
Fill in Health Score, Fault Probabilities, Energy ledger, Latency and Cost and Recent Alerts with template data
21d1612 verified
<!DOCTYPE html>
<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>