VE-CENTRAL

MASTER SERVER — MULTI-PROJECT SCADA PORTAL
`); w.document.close(); setTimeout(()=>w.print(),400); }); /* ---------- ALARM CENTRE ---------- */ function renderAlarmCentre(){ const tb = $('alarmTbody'); if(!tb) return; const sel = $('alarmFilter'); const cur = sel.value; sel.innerHTML = '' + visibleProjects().map(p=>``).join(''); sel.value = cur; const rows = []; visibleProjects().forEach(p=>{ if(cur && cur !== p.id) return; (telemetry[p.id]?.alarms||[]).forEach(a=>rows.push({...a, proj:p.name})); }); rows.sort((a,b)=>b.id-a.id); tb.innerHTML = rows.slice(0,200).map(a=>` ${a.time}${a.proj}${a.component}${a.desc} ${a.sev.toUpperCase()} ${a.ack?'ACK':'NEW'}`).join('') || 'No alarms.'; } $('alarmFilter').addEventListener('change', renderAlarmCentre); $('ackAllAlarms').addEventListener('click', ()=>{ visibleProjects().forEach(p=>(telemetry[p.id]?.alarms||[]).forEach(a=>a.ack=true)); audit('ACK ALARMS','ALL VISIBLE PROJECTS',''); renderAll(); toast('All visible alarms acknowledged.'); }); /* ---------- ENERGY ---------- */ function renderEnergy(){ const tb = $('energyTbody'); if(!tb) return; const list = visibleProjects(); tb.innerHTML = list.map(p=>{ const t = telemetry[p.id]; return `${p.name}${t.online?fmt(t.kw):'—'}${fmt(t.kwhToday,1)} ₹${fmt(t.kwhToday*SETTINGS.tariff,0)}${p.chambers} ${fmt(t.kwhToday/p.chambers,1)}`; }).join(''); const svg = $('energyChart'); const max = Math.max(...list.map(p=>telemetry[p.id].kw), 1); const bw = 860/Math.max(list.length,1); svg.innerHTML = '' + list.map((p,i)=>{ const t = telemetry[p.id]; const h = (t.kw/max)*180, x = 34+i*bw, w = bw*0.6; return ` ${fmt(t.kw,0)} ${p.name.split(' ')[0]}`; }).join(''); } /* ---------------- FLEET REPORT ---------------- */ function reportProjects(){ const scope = $('repScope') ? $('repScope').value : 'all'; const list = visibleProjects(); return scope === 'all' ? list : list.filter(p => p.id === scope); } function reportData(){ const list = reportProjects(); let running=0, alarms=0, kwh=0, kw=0; const rows = list.map(p=>{ const t = telemetry[p.id]; const crit = t.alarms.filter(a=>a.sev==='critical').length; const warn = t.alarms.filter(a=>a.sev==='warning').length; const info = t.alarms.filter(a=>a.sev==='info').length; const unack = t.alarms.filter(a=>!a.ack && a.sev!=='info').length; const avg = t.chambers.reduce((s,c)=>s+c.temp,0)/t.chambers.length; if(t.online && t.plantRunning) running++; alarms += unack; kwh += t.kwhToday; kw += t.online ? t.kw : 0; return {p, t, crit, warn, info, unack, avg, latest: t.alarms[0] || null}; }); return {rows, running, alarms, kwh, kw, list}; } function renderReport(){ if(!$('rpStatusTbody')) return; const sel = $('repScope'); if(sel){ const cur = sel.value; sel.innerHTML = '' + visibleProjects().map(p=>``).join(''); if(cur && Array.from(sel.options).some(o=>o.value===cur)) sel.value = cur; } const d = reportData(); $('rpSites').textContent = d.rows.length; $('rpRunning').textContent = d.running + '/' + d.rows.length; $('rpAlarms').textContent = d.alarms; $('rpEnergy').textContent = fmt(d.kwh,0); $('rpCost').textContent = '₹' + fmt(d.kwh*SETTINGS.tariff,0); $('rpStatusTbody').innerHTML = d.rows.map(r=>` ${r.p.name}
${r.p.id}
${r.p.client}${r.p.loc} ${r.t.online?'ONLINE':'OFFLINE'} ${r.t.plantRunning&&r.t.online?'RUNNING':'STOPPED'} ${r.t.online?r.t.compRunning:0}/${r.p.compressors} ${r.t.online?fmt(r.avg)+' °C':'—'} ${r.t.online?fmt(r.t.kw):'—'} ${r.unack>0?`${r.unack}`:'0'}`).join(''); $('rpEnergyTbody').innerHTML = d.rows.map(r=>`${r.p.name} ${r.t.online?fmt(r.t.kw):'—'}${fmt(r.t.kwhToday,1)} ₹${fmt(r.t.kwhToday*SETTINGS.tariff,0)}${r.p.chambers} ${fmt(r.t.kwhToday/r.p.chambers,1)}`).join('') + `TOTAL${fmt(d.kw,1)} ${fmt(d.kwh,1)}₹${fmt(d.kwh*SETTINGS.tariff,0)}`; $('rpTempTbody').innerHTML = d.rows.flatMap(r=>r.t.chambers.map((c,i)=> `${i===0?r.p.name:''}Chamber-${i+1}${fmt(c.temp)} °C ${fmt(c.rh,0)} % ${c.temp>-14?'ABOVE SP':'IN RANGE'}`)).join(''); $('rpAlarmTbody').innerHTML = d.rows.map(r=>`${r.p.name} ${r.crit}${r.warn}${r.info} ${r.unack>0?`${r.unack}`:'0'} ${r.latest? r.latest.component+' — '+r.latest.desc : '—'}`).join(''); const detail = []; d.rows.forEach(r=>r.t.alarms.slice(0,8).forEach(a=>detail.push({...a, proj:r.p.name}))); detail.sort((a,b)=>b.id-a.id); $('rpAlarmDetail').innerHTML = detail.slice(0,60).map(a=>`${a.time}${a.proj} ${a.component}${a.desc} ${a.sev.toUpperCase()}`).join('') || 'No alarms recorded.'; } if($('repScope')) $('repScope').addEventListener('change', renderReport); function plainTable(headers, rows){ return '' + headers.map(x=>``).join('') + '' + rows.map(r=>'' + r.map(c=>``).join('') + '').join('') + '
${x}
${c}
'; } if($('repPrint')) $('repPrint').addEventListener('click',()=>{ renderReport(); const d = reportData(); const scopeTxt = $('repScope').value==='all' ? 'All projects' : projName($('repScope').value); const statusRows = d.rows.map(r=>[r.p.name, r.p.client, r.p.loc, r.t.online?'ONLINE':'OFFLINE', (r.t.plantRunning&&r.t.online)?'RUNNING':'STOPPED', (r.t.online?r.t.compRunning:0)+'/'+r.p.compressors, r.t.online?fmt(r.avg)+' °C':'—', r.t.online?fmt(r.t.kw):'—', r.unack]); const energyRows = d.rows.map(r=>[r.p.name, r.t.online?fmt(r.t.kw):'—', fmt(r.t.kwhToday,1), '₹'+fmt(r.t.kwhToday*SETTINGS.tariff,0), r.p.chambers, fmt(r.t.kwhToday/r.p.chambers,1)]); energyRows.push(['TOTAL',''+fmt(d.kw,1)+'',''+fmt(d.kwh,1)+'', '₹'+fmt(d.kwh*SETTINGS.tariff,0)+'','','']); const tempRows = d.rows.flatMap(r=>r.t.chambers.map((c,i)=> [i===0?r.p.name:'', 'Chamber-'+(i+1), fmt(c.temp)+' °C', fmt(c.rh,0)+' %', c.temp>-14?'ABOVE SP':'IN RANGE'])); const alarmRows = d.rows.map(r=>[r.p.name, r.crit, r.warn, r.info, r.unack, r.latest? r.latest.component+' — '+r.latest.desc : '—']); const detail = []; d.rows.forEach(r=>r.t.alarms.slice(0,8).forEach(a=>detail.push({...a, proj:r.p.name}))); detail.sort((a,b)=>b.id-a.id); const detailRows = detail.slice(0,50).map(a=>[a.time,a.proj,a.component,a.desc,a.sev.toUpperCase()]); const mapSvg = $('indiaMap') ? $('indiaMap').outerHTML : ''; const w = window.open('','_blank'); w.document.write(`Fleet Report — ${SETTINGS.org}
${SETTINGS.org} — Central Master Server
Project Executed By

Consolidated Fleet Report

Scope: ${scopeTxt}  |  Generated: ${new Date().toLocaleString()}  |  By: ${SESSION.username} (${SESSION.role})  |  Tariff: ₹${SETTINGS.tariff}/kWh
${d.rows.length}PROJECTS
${d.running}/${d.rows.length}PLANTS RUNNING
${d.alarms}ACTIVE ALARMS
${fmt(d.kwh,0)}TOTAL kWh TODAY
₹${fmt(d.kwh*SETTINGS.tariff,0)}ENERGY COST

1) Plant Status Summary

${plainTable(['Project','Client','Location','Link','Plant','Compressors','Avg Temp','Load kW','Unack Alarms'], statusRows)}

2) Energy Summary

${plainTable(['Project','Live Load (kW)','Energy Today (kWh)','Cost','Chambers','kWh / Chamber'], energyRows)}

3) Chamber Temperature Summary

${plainTable(['Project','Chamber','Temperature','RH','Status'], tempRows)}

4) Alarm Summary by Project

${plainTable(['Project','Critical','Warning','Info','Unacknowledged','Latest Alarm'], alarmRows)}

5) Recent Alarm Detail

${plainTable(['Time','Project','Component','Description','Severity'], detailRows)} ${mapSvg?`

6) Site Locations

${mapSvg}

Map outline is a simplified schematic for site location only and is not an official boundary depiction.

`:''}

Values in this report are generated by the portal's simulation layer for demonstration. Figures from a live deployment would come from the site historian.

Project Executed By
`); w.document.close(); audit('FLEET REPORT PRINTED', scopeTxt, d.rows.length+' projects'); setTimeout(()=>w.print(),500); }); if($('repCsv')) $('repCsv').addEventListener('click',()=>{ const d = reportData(); let csv = 'CONSOLIDATED FLEET REPORT\n'; csv += 'Organisation,'+SETTINGS.org+'\nGenerated,'+new Date().toLocaleString()+'\nBy,'+SESSION.username+'\n\n'; csv += '1) PLANT STATUS\nProject,Client,Location,Link,Plant,Compressors,Avg Temp C,Load kW,Unack Alarms\n'; d.rows.forEach(r=>csv += `"${r.p.name}","${r.p.client}","${r.p.loc}",${r.t.online?'ONLINE':'OFFLINE'},` + `${(r.t.plantRunning&&r.t.online)?'RUNNING':'STOPPED'},${r.t.online?r.t.compRunning:0}/${r.p.compressors},` + `${fmt(r.avg)},${r.t.online?fmt(r.t.kw):''},${r.unack}\n`); csv += '\n2) ENERGY\nProject,Live kW,kWh Today,Cost,Chambers,kWh per Chamber\n'; d.rows.forEach(r=>csv += `"${r.p.name}",${fmt(r.t.kw)},${fmt(r.t.kwhToday,2)},` + `${fmt(r.t.kwhToday*SETTINGS.tariff,0)},${r.p.chambers},${fmt(r.t.kwhToday/r.p.chambers,2)}\n`); csv += `TOTAL,${fmt(d.kw,2)},${fmt(d.kwh,2)},${fmt(d.kwh*SETTINGS.tariff,0)},,\n`; csv += '\n3) CHAMBER TEMPERATURES\nProject,Chamber,Temp C,RH %\n'; d.rows.forEach(r=>r.t.chambers.forEach((c,i)=>csv += `"${r.p.name}",Chamber-${i+1},${fmt(c.temp)},${fmt(c.rh,0)}\n`)); csv += '\n4) ALARM SUMMARY\nProject,Critical,Warning,Info,Unacknowledged\n'; d.rows.forEach(r=>csv += `"${r.p.name}",${r.crit},${r.warn},${r.info},${r.unack}\n`); const b = new Blob([csv],{type:'text/csv'}); const el = document.createElement('a'); el.href = URL.createObjectURL(b); el.download = 'fleet_report_' + new Date().toISOString().slice(0,10) + '.csv'; el.click(); audit('FLEET REPORT EXPORTED','CSV', d.rows.length+' projects'); }); /* ---------- USERS ---------- */ function renderUsers(){ const tb = $('userTbody'); if(!tb || !isAdmin()) return; tb.innerHTML = USERS.map(u=>{ const acc = u.projects === 'ALL' ? 'ALL PROJECTS' : (u.projects.length ? u.projects.map(id=>`${projName(id)}`).join('') : 'none'); return ` ${u.username} ${u.name||'—'}
${u.company||''}
${u.role} ${acc} ${u.active?'ACTIVE':'DISABLED'} ${new Date(u.created).toLocaleDateString()} ${u.lastLogin||'never'}
${u.username!=='admin'?``:''}
`; }).join(''); tb.querySelectorAll('[data-edit]').forEach(b=>b.addEventListener('click',()=>userForm(b.dataset.edit))); tb.querySelectorAll('[data-toggle]').forEach(b=>b.addEventListener('click',()=>{ const u = USERS.find(x=>x.id===b.dataset.toggle); if(u.username==='admin' && u.active){ toast('The primary admin account cannot be disabled.'); return; } u.active = !u.active; persist(); audit(u.active?'USER ENABLED':'USER DISABLED', u.username,''); renderUsers(); toast('Account '+(u.active?'enabled':'disabled')+'.'); })); tb.querySelectorAll('[data-del]').forEach(b=>b.addEventListener('click',()=>{ const u = USERS.find(x=>x.id===b.dataset.del); if(!confirm('Delete account "'+u.username+'"? This cannot be undone.')) return; USERS = USERS.filter(x=>x.id!==u.id); persist(); audit('USER DELETED', u.username,''); renderUsers(); toast('Account deleted.'); })); tb.querySelectorAll('[data-reset]').forEach(b=>b.addEventListener('click',()=>{ const u = USERS.find(x=>x.id===b.dataset.reset); const pw = genPassword(); u.salt = newSalt(); u.pass = hashPw(pw, u.salt); u.mustChange = true; persist(); audit('PASSWORD RESET', u.username,''); showCredentials(u, pw, 'Password reset'); })); } function userForm(editId){ const u = editId ? USERS.find(x=>x.id===editId) : null; const projChips = PROJECTS.map(p=>{ const sel = u && u.projects !== 'ALL' && u.projects.includes(p.id); return `${p.name}`; }).join(''); $('userModalBody').innerHTML = ` ×

${u?'Edit Account':'Create Customer Login'}

${u?u.username:'A password will be generated automatically'}
${projChips}
`; $('userModal').classList.add('show'); wireModal('userModal'); $('userModalBody').querySelectorAll('[data-proj]').forEach(c=>c.addEventListener('click',()=>c.classList.toggle('sel'))); $('fuSave').addEventListener('click', ()=>{ const username = u ? u.username : $('fuUser').value.trim(); if(!username){ toast('Username is required.'); return; } if(!u && USERS.some(x=>x.username.toLowerCase()===username.toLowerCase())){ toast('That username already exists.'); return; } const role = $('fuRole').value; const all = $('fuAll').checked || role==='ADMIN'; const projects = all ? 'ALL' : Array.from($('userModalBody').querySelectorAll('[data-proj].sel')).map(c=>c.dataset.proj); if(!all && projects.length===0){ toast('Select at least one project, or grant all access.'); return; } if(u){ u.name=$('fuName').value.trim(); u.company=$('fuCompany').value.trim(); u.role=role; u.projects=projects; persist(); audit('USER UPDATED', u.username, role); $('userModal').classList.remove('show'); renderUsers(); toast('Account updated.'); } else { const pw = genPassword(), salt = newSalt(); const nu = {id:'U-'+String(USERS.length+1).padStart(3,'0'), username, name:$('fuName').value.trim(), company:$('fuCompany').value.trim(), role, projects, active:true, salt, pass:hashPw(pw,salt), created:new Date().toISOString(), lastLogin:null, mustChange:true}; USERS.push(nu); persist(); audit('USER CREATED', username, role+' · '+(projects==='ALL'?'ALL':projects.join(','))); renderUsers(); showCredentials(nu, pw, 'Login created'); } }); } $('addUserBtn').addEventListener('click', ()=>userForm(null)); function showCredentials(u, pw, heading){ const scope = u.projects==='ALL' ? 'All projects' : u.projects.map(projName).join(', '); const text = `VE-CENTRAL — SCADA Portal Access Organisation : ${SETTINGS.org} -------------------------------- Username : ${u.username} Password : ${pw} Role : ${u.role} Access : ${scope} -------------------------------- Please sign in and change this password immediately. Do not share these credentials by unsecured channels.`; $('userModalBody').innerHTML = ` ×

${heading}

Share these details with the customer
This password is shown once and is not recoverable afterwards — copy it now. Send it through a channel the customer already trusts, and ask them to change it at first sign-in.
${text}
`; $('userModal').classList.add('show'); wireModal('userModal'); $('copyCred').addEventListener('click', ()=>{ navigator.clipboard?.writeText(text).then( ()=>toast('Credentials copied to clipboard.'), ()=>toast('Copy failed — select the text manually.')); }); $('printCred').addEventListener('click', ()=>{ const w = window.open('','_blank'); w.document.write(`Portal Access — ${u.username}
${SETTINGS.org} — SCADA Portal
Project Executed By

Portal Access Handover

${text}

Issued ${new Date().toLocaleString()} by ${SESSION.username}.

`); w.document.close(); setTimeout(()=>w.print(),300); }); } /* ---------- AUDIT ---------- */ function renderAudit(){ const tb = $('auditTbody'); if(!tb || !isAdmin()) return; tb.innerHTML = AUDIT.slice(0,300).map(a=>`${a.ts}${a.user} ${a.action} ${a.target}${a.detail}`).join('') || 'No entries.'; } $('exportAudit').addEventListener('click', ()=>{ let csv = 'Timestamp,User,Action,Target,Detail\n'; AUDIT.forEach(a=>csv += `"${a.ts}","${a.user}","${a.action}","${a.target}","${a.detail}"\n`); const b = new Blob([csv],{type:'text/csv'}); const el = document.createElement('a'); el.href = URL.createObjectURL(b); el.download = 've_central_audit.csv'; el.click(); }); /* ---------- SETTINGS ---------- */ $('saveSettings').addEventListener('click', ()=>{ SETTINGS = {org:$('setOrg').value.trim()||'Vishwa Enterprises', poll:Math.max(2,+$('setPoll').value||5), tariff:+$('setTariff').value||8, idle:Math.max(1,+$('setIdle').value||30)}; persist(); audit('SETTINGS UPDATED','portal',''); restartPolling(); toast('Settings saved.'); }); $('backupBtn').addEventListener('click', ()=>{ const data = JSON.stringify({projects:PROJECTS, users:USERS, audit:AUDIT, settings:SETTINGS}, null, 2); const b = new Blob([data],{type:'application/json'}); const el = document.createElement('a'); el.href = URL.createObjectURL(b); el.download = 've_central_backup.json'; el.click(); audit('BACKUP EXPORTED','portal',''); }); $('restoreBtn').addEventListener('click', ()=>$('restoreFile').click()); $('restoreFile').addEventListener('change', e=>{ const f = e.target.files[0]; if(!f) return; const r = new FileReader(); r.onload = () => { try{ const d = JSON.parse(r.result); if(d.projects) PROJECTS = d.projects; if(d.users) USERS = d.users; if(d.audit) AUDIT = d.audit; if(d.settings) SETTINGS = d.settings; PROJECTS.forEach(p=>{ if(!telemetry[p.id]) initTelemetry(p); }); persist(); audit('BACKUP RESTORED','portal',f.name); renderAll(); toast('Backup restored.'); }catch(err){ toast('Restore failed — invalid file.'); } }; r.readAsText(f); }); $('resetBtn').addEventListener('click', ()=>{ if(!confirm('Reset the portal? All projects, accounts and audit history will be erased.')) return; try{ ['ve_projects','ve_users','ve_audit','ve_settings'].forEach(k=>localStorage.removeItem(k)); }catch(e){} location.reload(); }); function renderSettings(){ if(!isAdmin()) return; $('setOrg').value = SETTINGS.org; $('setPoll').value = SETTINGS.poll; $('setTariff').value = SETTINGS.tariff; $('setIdle').value = SETTINGS.idle; $('dmProjects').textContent = PROJECTS.length; $('dmUsers').textContent = USERS.length; $('dmAudit').textContent = AUDIT.length; } /* ---------- ACCOUNT ---------- */ function renderAccount(){ if(!SESSION) return; $('acUser').textContent = SESSION.username; $('acRole').textContent = SESSION.role; $('acProjects').textContent = SESSION.projects==='ALL' ? 'All projects' : SESSION.projects.map(projName).join(', ') || 'none'; $('acLast').textContent = SESSION.lastLogin || '—'; } $('changePwBtn').addEventListener('click', ()=>{ const o = $('pwOld').value, n = $('pwNew').value, n2 = $('pwNew2').value; if(hashPw(o, SESSION.salt) !== SESSION.pass){ toast('Current password is incorrect.'); return; } if(n.length < 8){ toast('New password must be at least 8 characters.'); return; } if(n !== n2){ toast('New passwords do not match.'); return; } SESSION.salt = newSalt(); SESSION.pass = hashPw(n, SESSION.salt); SESSION.mustChange = false; persist(); audit('PASSWORD CHANGED', SESSION.username,''); $('pwOld').value = $('pwNew').value = $('pwNew2').value = ''; toast('Password updated.'); }); /* ---------- render + polling ---------- */ function renderAll(){ if(!SESSION) return; renderFleet(); renderProjects(); renderMap(); renderAlarmCentre(); renderEnergy(); renderReport(); if(isAdmin()){ renderUsers(); renderAudit(); renderSettings(); } renderAccount(); } let pollTimer = null; function restartPolling(){ clearInterval(pollTimer); pollTimer = setInterval(()=>{ if(SESSION){ tickTelemetry(); renderAll(); } }, (SETTINGS.poll||5)*1000); } restartPolling(); /* page credit block on every page */ document.querySelectorAll('.page').forEach(pg=>{ const d = document.createElement('div'); d.className = 'page-credit'; d.innerHTML = `
Project Executed By
`; pg.appendChild(d); });