NH3
AMMONIA REFRIGERATION SCADA
MULTI-CHAMBER COLD STORAGE — PLANT CONTROL SYSTEM
SYSTEM NORMAL
PLC: ONLINE
0 ACTIVE ALARMS
🔒 OPERATOR
--:--:--
SCADA Dashboard
SYSTEM CONTROL — COMPRESSORS / CHAMBERS / LPR / CONDENSER
0/3
COMPRESSORS RUNNING
0%
AVG SYSTEM LOAD
0.0 kW
TOTAL POWER DRAW
0
ACTIVE ALARMS
PLANT MASTER SEQUENCE IDLE
Runs the full plant start/stop interlock sequence automatically, one step at a time, with feedback checks (e.g. WFS confirmation, evaporator solenoid ON) before proceeding.
AMMONIA RECIPROCATING COMPRESSORS
COLD STORAGE CHAMBERS — EVAPORATOR UNITS
LOW PRESSURE AMMONIA RECEIVER (LPR) MASTER/SLAVE PUMP CONTROL
Master Pump
PUMP-1PUMP-2
Main Liquid Line Solenoid (HPR → LPR)
Normally sequenced automatically; requires ≥1 evaporator solenoid ON before the LPR pump can start.
--
High Level Float protects compressor — liquid hammer--
Middle Level Float controls makeup solenoid--
Low Level Float protects pump — dry run--
Liquid Makeup Solenoid Valve--
EVAPORATIVE CONDENSER MASTER/SLAVE PUMP CONTROL
Master Pump
PUMP-1PUMP-2
Water Flow Switch (WFS)--
Interlocked: any running compressor requires proven condenser water flow.
Fan 1-3 Auto Control (by Head Pressure)
Process & Instrumentation Diagram
MIMIC LAYOUT — 3× COMPRESSOR / OIL SEPARATOR / HPR / CHAMBERS / CONDENSER
Energy System
EQUIPMENT-WISE VOLTAGE / CURRENT / POWER FACTOR / CONSUMPTION
0.0 kW
TOTAL PLANT LOAD
Equipment TagStatusVoltage (V)Current (A)Power FactorConsumption (kW)
Ammonia Detection
GAS SENSOR NETWORK — PPM MONITORING & SAFETY INTERLOCKS
NH3 SAFETY INTERLOCKS
MAX AMMONIA DETECTED
0.0 PPM
SAFETY THRESHOLD MATRIX EDITABLE LIMITS
Warning Threshold (PPM) → Ventilation Start
Alarm Threshold (PPM) → Audio/Visual Siren
Critical Trip (PPM) → Plant Emergency Shutdown
VentilationOFF
Siren / BeaconOFF
Emergency ShutdownNORMAL
Sensor TagTarget LocationPPM LevelStatusLast Calibration
Floor Plan
PLANT LAYOUT — CHAMBER & MACHINE ROOM OVERVIEW
MACHINE ROOM Compressors · LPR · Panel EVAPORATIVE CONDENSER YARD
Blast Freezer Freezing Cycle
CYCLE PROFILE — CHAMBER TEMPERATURE VS TIME — BATCH TRACKING
Cycle Control
Batch / Lot No.
Product Description
Cycle StageIDLE
Elapsed Time00:00
Product Core Temp--
Set Point-35.0 °C
Cycle Stages
1. Pull-down (0 → -18°C)~40 min
2. Deep Freeze (-18 → -30°C)~90 min
3. Hold / Equalize (-35°C)~30 min
4. Defrost & Release~15 min
Temperature Trend no active batch
Batch History & Printable Report
Batch No.ProductStartEndDurationStatusStart / End Core (°C)
Select a batch above and click Print Batch Report for a printable/PDF record of that run, including its temperature trend and stage timing.
Door Opening Log
CHAMBER ACCESS EVENTS
TimeChamberEventDuration OpenOperator
Maintenance Schedule
DAILY / WEEKLY / MONTHLY TASK PLANNER
Data Logging
TREND RECORDING & EXPORT
Log Interval
Sample Every
Records Stored0
Export
Exports the in-memory log buffer shown below.
Scope
Chamber Temp/RH● logging
Compressor P/T● logging
System P/T● logging
All Chambers — Temperature Trend
Log Buffer — Chamber Temperatures & RH LATEST 25
TimeCh-1 °CCh-2 °CCh-3 °CCh-4 °CCh-5 °C
Log Buffer — Compressor Pressure Parameters SUCTION / OIL / DISCHARGE (bar)
Time COMPRESSOR-1 COMPRESSOR-2 COMPRESSOR-3
SuctOilDisch SuctOilDisch SuctOilDisch
Compressor Pressure Trend
COMP-1COMP-2COMP-3
■ Suction   ■ Oil   ■ Discharge
System Settings
SET POINTS & CONTROL DELAYS
Chamber Temperature Range
Condenser Fan ON/OFF Pressure Settings
Fan-1 ON Pressure bar
Fan-2 ON Pressure bar
Fan-3 ON Pressure bar
Compressor Load / Unload Pressure Settings — Per Piston (4 Pistons Each) WITH ON/OFF TIME DELAY
Access Control PASSWORD PROTECTION
Current access levelOPERATOR
Auto-lock after inactivity min
Protected pages Settings · Calibration · Scaling · QR (Engineer)  |  DI/DO · Panel GA · Maintenance (Supervisor)
Default passwords — Supervisor 2468, Engineer 1357. Change both before handover. This is an operator-discipline control inside the browser, not a security boundary: it prevents accidental or unauthorised set-point changes, but anyone with file access can bypass it. Enforce real access control at the PLC/HMI and server.
Defrost Parameters APPLIES TO ALL EVAPORATORS
Coil Termination Temperature °C
Maximum Defrost Time min
Drip / Drain Delay min
Pump-Down Delay (solenoid closed → heater ON) sec
Fan OFF Delay (within pump-down) sec
Defrost ends on whichever occurs first — coil reaching termination temperature, or maximum time elapsing.
Plant Master Sequence — Timers SETTABLE ON/OFF DELAY PER STEP
START SEQUENCE — ON DELAY BEFORE EACH STEP
STOP SEQUENCE — OFF DELAY BEFORE EACH STEP
Compressor Unload Step Delay (between 100→75→50→25→0%) sec
LPR Pump DPS Bypass / ON Delay sec
Analog Calibration
4–20 mA RAW VALUE / OFFSET BY TAG ID
Tag IDParameterRaw @4mARaw @20mAOffsetLive ValueAction
Analog Scaling
MANUAL MIN / MAX ENGINEERING RANGE PER SENSOR (4mA = MIN, 20mA = MAX)
Tag IDParameterUnitMin Range (4mA)Max Range (20mA)Live Scaled ValueAction
Sets the engineering-unit range each transmitter's 4–20 mA signal is scaled across. Values are validated (min must be less than max) before being applied.
DI / DO Diagnosis
FEEDER SELECTOR / STATUS / TRIP INDICATION
FeederSelector PositionON/OFF StatusTrip Indication
Electrical Control Panel GA
GENERAL ARRANGEMENT — FEEDER LAYOUT
Machinery Runtime Log
CUMULATIVE OPERATING HOURS
EquipmentStatusToday (hrs)Total Runtime (hrs)Last Start
Active Alarms & System Events
REAL-TIME AUDIT LOG & SAFETY ALARMS
Real-Time Audit Log 0 events
System Safety Alarm Thresholds
High Chamber Temp> -12 °C
Low Chamber Temp< -38 °C
High Discharge Pressure> 16.5 bar
Low Suction Pressure< 0.8 bar
Ammonia PPM High> 100 ppm
Ammonia PPM Critical> 300 ppm
QR Access
SCAN-TO-OPEN CODE FOR THIS SCADA INTERFACE
Generate QR Code
Hosted page URL
Encoded —
Error correction level H (30% recovery) so the centre logo does not prevent scanning. The QR encodes a link — the page itself must be reachable at that URL.
Scan to Open
Daily Summary Report
ENERGY · ALARMS · TEMPERATURE · DOOR LOG — CONSOLIDATED SHIFT REPORT
Report Date--
Generated At--
Coverage Window--
PlantAmmonia Cold Storage — Site 1
0.0 kWh
TOTAL ENERGY TODAY
0
TOTAL ALARM EVENTS
0
CRITICAL ALARMS
0
DOOR OPEN EVENTS
0.0 h
TOTAL MACHINE RUN HOURS
1) ENERGY CONSUMPTION REPORT
Equipment TagStatus NowAvg Power (kW)Energy Today (kWh)Est. Cost (@ ₹8/kWh)
2) ALARM REPORT
0
CRITICAL
0
WARNING
0
INFO / EVENT
TimeTagComponentDescriptionSeverity
3) TEMPERATURE LOG SUMMARY
ChamberMin Temp (°C)Max Temp (°C)Avg Temp (°C)Current (°C)Current RH (%)
4) DOOR OPENING LOG SUMMARY
ChamberOpen EventsTotal Open Duration
TimeChamberEventDurationOperator
5) MACHINERY-WISE DAILY RUNNING HOURS
EquipmentStatus NowRunning Hours TodayTotal Runtime (hrs)Utilisation %
Report is compiled from live in-session simulated data and resets when this browser tab is closed or reloaded.

Restricted Access

Enter password to continue

⏻ EMERGENCY STOP ACTIVE

All compressors, evaporators, pumps and fans have been commanded to STOP. Clear the field condition and press RESET on the header bar to restore control.

`); w.document.close(); setTimeout(()=>w.print(),300); } const energyPrintBtn=document.getElementById('energyPrintDay'); if(energyPrintBtn) energyPrintBtn.addEventListener('click',()=>{ printEnergyDayReport(energyDaySel.value || dateKey(new Date())); }); /* ---------------- DAILY ACCUMULATORS (feed the Daily Summary Report) ---------------- */ const ACCUM_INTERVAL_SEC = 2; // matches the setInterval cadence below function updateDailyAccumulators(){ // energy: kWh += kW * (interval_sec/3600) for every equipment item equipmentList().forEach(e=>{ const kw = e.running ? (e.kw!==undefined && e.kw ? e.kw : e.base*(0.9+Math.random()*0.2)) : 0; if(!state.dailyEnergyKwh[e.tag]) state.dailyEnergyKwh[e.tag]={kwh:0,sampleKw:0,samples:0}; const acc=state.dailyEnergyKwh[e.tag]; acc.kwh += kw*(ACCUM_INTERVAL_SEC/3600); acc.sampleKw += kw; acc.samples++; }); // chamber temperature min/max/avg state.chambers.forEach(ch=>{ if(!state.chamberDailyStats[ch.id]) state.chamberDailyStats[ch.id]={min:Infinity,max:-Infinity,sum:0,count:0}; const s=state.chamberDailyStats[ch.id]; s.min=Math.min(s.min,ch.temp); s.max=Math.max(s.max,ch.temp); s.sum+=ch.temp; s.count++; }); } /* ---------------- AMMONIA DETECTION ---------------- */ const ammoniaSensors=[ {tag:'AGS-01',loc:'Machine Room'},{tag:'AGS-02',loc:'Machine Room'}, {tag:'AGS-03',loc:'Low PIT Area'},{tag:'AGS-04',loc:'High Pressure Receiver'}, {tag:'AGS-05',loc:'Chamber 01'},{tag:'AGS-06',loc:'Chamber 02'}, {tag:'AGS-07',loc:'Chamber 03'},{tag:'AGS-08',loc:'Chamber 04'}, {tag:'AGS-09',loc:'Chamber 05'},{tag:'AGS-10',loc:'Ante Room'}, ].map(s=>({...s,ppm:Math.random()*8,lastCal:'2026-06-'+String(10+Math.floor(Math.random()*15)).padStart(2,'0')})); function renderAmmonia(){ const tb=document.getElementById('ammoniaTbody'); tb.innerHTML=''; let maxPpm=0; ammoniaSensors.forEach(s=>{ s.ppm += (Math.random()-0.5)*2; s.ppm=Math.max(0,Math.min(350,s.ppm)); if(Math.random()<0.0015) s.ppm += 150; // rare simulated leak spike maxPpm=Math.max(maxPpm,s.ppm); let status='NORMAL'; if(s.ppm>300){status='CRITICAL';} else if(s.ppm>100){status='HIGH';} tb.innerHTML+=`${s.tag}${s.loc}${fmt(s.ppm,1)} ppm${status}${s.lastCal}`; }); state.ammonia.maxPpm=maxPpm; applyAmmoniaInterlocks(); renderAmmoniaSafetyPanel(); } /* ---------------- NH3 SAFETY INTERLOCKS ---------------- */ function applyAmmoniaInterlocks(){ const a=state.ammonia, t=a.thresholds, ppm=a.maxPpm; // Ventilation Start — edge-triggered so it only logs on state change const ventShould = ppm>=t.warning; if(ventShould && !a.ventilationOn){ a.ventilationOn=true; pushAlarm('NH3 SAFETY','Warning threshold reached — ventilation system started','warning'); } else if(!ventShould && a.ventilationOn){ a.ventilationOn=false; pushAlarm('NH3 SAFETY','PPM below warning threshold — ventilation stopped','info'); } // Audio/Visual Siren const sirenShould = ppm>=t.alarm; if(sirenShould && !a.sirenActive){ a.sirenActive=true; pushAlarm('NH3 SAFETY','Alarm threshold reached — audio/visual siren activated','critical'); } else if(!sirenShould && a.sirenActive && ppm=t.critical && !a.criticalTripped){ a.criticalTripped=true; pushAlarm('NH3 SAFETY','CRITICAL TRIP — ammonia concentration exceeded critical limit. PLANT EMERGENCY SHUTDOWN.','critical'); triggerEmergencyStop('NH3 critical gas concentration'); } } function renderAmmoniaSafetyPanel(){ const el=id=>document.getElementById(id); if(!el('nh3MaxPpm'))return; const a=state.ammonia; el('nh3MaxPpm').textContent=fmt(a.maxPpm,1)+' PPM'; el('nh3MaxPpm').style.color = a.maxPpm>=a.thresholds.critical?'var(--red)':a.maxPpm>=a.thresholds.alarm?'var(--amber)':'var(--green)'; el('nh3VentBadge').className='run-badge '+(a.ventilationOn?'on':'off'); el('nh3VentBadge').textContent=a.ventilationOn?'RUNNING':'OFF'; const sirenTestOn = Date.now(){ const w=parseFloat(document.getElementById('thWarning').value); const al=parseFloat(document.getElementById('thAlarm').value); const cr=parseFloat(document.getElementById('thCritical').value); if(isNaN(w)||isNaN(al)||isNaN(cr)||!(w{ state.ammonia.sirenTestUntil=Date.now()+4000; pushAlarm('NH3 SAFETY','Siren / beacon test initiated by operator','info'); renderAlarms(); renderAmmoniaSafetyPanel(); setTimeout(renderAmmoniaSafetyPanel,4100); }); /* ---------------- FLOOR PLAN + P&ID CHAMBERS ---------------- */ function renderFloorPlan(){ const g=document.getElementById('floorChambers'); if(!g)return; g.innerHTML=''; state.chambers.forEach((ch,i)=>{ const x=20+ (i%3)*220, y=20+Math.floor(i/3)*195; const active=ch.evaps.some(e=>e.running); g.innerHTML+=` CHAMBER-${ch.id} ${fmt(ch.temp)}°C RH ${fmt(ch.rh,0)}%`; }); } /* ---------------- P&ID MIMIC (matches uploaded plant layout) ---------------- */ function pidCompressorBlock(c,x,y){ const onoff=(v)=>v?'ON':'OFF'; const sp=c.suctP!==undefined?fmt(c.suctP,2):'--', dp=c.discP!==undefined?fmt(c.discP,2):'--', op=c.oilP!==undefined?fmt(c.oilP,2):'--'; return ` OIL SEPARATOR COMPRESSOR-${c.id} · ${c.role}${c.olr?' ⚠ OLR TRIP':''} Suction pressure = ${sp} Bar Discharge pressure = ${dp} Bar Oil pressure = ${op} Bar Comp. Start = ${onoff(c.running)} Comp. SV-1 = ${onoff(c.sv1)} Comp. SV-2 = ${onoff(c.sv2)} Load: ${c.running?c.load+'%':'--'} `; } function renderPidMimic(){ const svg=document.getElementById('pidMimicSvg'); if(!svg)return; let html=''; // three compressor / oil separator blocks across the top state.compressors.forEach((c,i)=>{ html+=pidCompressorBlock(c,20+i*340,16); }); // HPR vessel (SA 516 Gr 70, 30" ID x 4000 length x 12mm thick + 14mm dish ends) const hprY=250; html+=` HIGH PRESSURE RECEIVER (HPR) SA 516 Gr 70 · SIZE: 30" ID x 4000 LENGTH x 12mm THICK + 14mm DISH ENDS HPR pressure = ${fmt(state.hprPressure,2)} Bar Level: STC-15 LLG · STC-15 · STC-15 Tag: B1.03 · PG AV `; // chambers row const chY=hprY+110, chW=(1000-20)/state.chambers.length; state.chambers.forEach((ch,i)=>{ const x=20+i*chW; const active=ch.evaps.some(e=>e.running); html+=` CHAMBER - ${ch.id} Temperature = ${fmt(ch.temp)} Deg.C Humidity RH% = ${fmt(ch.rh,0)} % `; }); // bottom row: condenser/LPR line, WLS/WFS, ambient, water temp const botY=chY+120; const wlsOk=state.cond.wls, wfsOk=state.cond.wfs; html+=` CONDENSER / LPR LINE Suction pressure = ${fmt(state.cond.suctP,2)} Bar Discharge pressure = ${fmt(state.cond.discP,2)} Bar WLS / WFS WLS: ${wlsOk?'NORMAL':'LOW'} WFS: ${wfsOk?'FLOW OK':'NO FLOW'} AMBIENT / WATER Ambient Temperature = ${fmt(state.ambientTemp)} Deg.C Water Temperature = ${fmt(state.waterTemp)} Deg.C `; svg.innerHTML=html + ` ${EXECUTED_BY_LABEL.toUpperCase()} `; } /* ---------------- MAINTENANCE SCHEDULE ---------------- */ const maintTasks={ Daily:[ 'Record compressor suction, discharge, oil pressure & temperature on log sheet', 'Check compressor oil level in crankcase / oil separator sight glass', 'Verify oil pressure differential across pump is within limits', 'Check ammonia gas detector readings on all zones (machine room, pit, chambers)', 'Inspect machine room ventilation / exhaust fan operation', 'Log condenser water make-up, bleed-off and basin water level', 'Check condenser water pump discharge pressure & spray nozzle distribution', 'Verify LPR liquid level and float switch indications', 'Record all chamber temperatures and RH against set points', 'Check for visible oil / ammonia leakage at flanges, glands and valve stems', 'Inspect evaporator coils for ice build-up; initiate defrost if required', 'Verify condensate drain lines and drip trays are clear', 'Check unusual noise / vibration on compressors, pumps and fans', 'Confirm emergency stop and ammonia alarm beacon are healthy', 'Verify chamber door seals closing properly; log door-open events' ], Weekly:[ 'Grease compressor and motor bearings as per lubrication chart', 'Check V-belt tension, alignment and condition; replace if frayed', 'Function-test emergency stop, audio/visual siren and beacon circuits', 'Clean condenser water strainers and Y-filters', 'Purge non-condensable gases from condenser / HPR', 'Check and clean evaporator fan guards and coil surfaces', 'Verify operation of all solenoid valves (manual override test)', 'Test differential pressure switch operation on LPR pumps', 'Check float switch operation — high, middle and low level', 'Inspect insulation on suction lines for damage or condensation', 'Check electrical panel for loose terminations and abnormal heating', 'Verify water treatment / dosing levels in condenser basin', 'Test standby compressor by running it for a short duty period', 'Backup SCADA trend data and alarm history' ], Monthly:[ 'Full ammonia leak test on all flange joints, valves and welds', 'Calibrate all pressure and temperature transmitters against reference', 'Draw compressor oil sample for analysis; top up or change as required', 'Insulation resistance (megger) test on all motors', 'Function-test safety relief valves and record set pressures', 'Calibrate ammonia gas detection sensors and record certificate', 'Clean condenser coil externally and descale as required', 'Check and tighten all foundation and anchor bolts', 'Inspect and test compressor capacity control (unloader) operation', 'Verify oil separator float / drain valve operation', 'Check compressor safety cut-outs — HP, LP, oil pressure trip settings', 'Inspect chamber door heaters, curtains and gaskets', 'Test emergency ventilation interlock with gas detection system', 'Review and update machinery runtime hours and service due list', 'Inspect earthing / bonding continuity of plant equipment', 'Verify operation of all interlocks: WFS, float, DPS and defrost' ] }; function renderMaint(){ const grid=document.getElementById('maintGrid'); grid.innerHTML=''; Object.entries(maintTasks).forEach(([freq,tasks])=>{ const card=document.createElement('div'); card.className='card'; card.innerHTML=`
${freq.toUpperCase()} ${tasks.length} TASKS
`+ `
`+ tasks.map((t,idx)=>`
${t}
`).join('')+ `
`; grid.appendChild(card); }); } /* ---------------- DOOR LOG ---------------- */ function seedDoorLog(){ const events=[]; const now=Date.now(); for(let i=0;i<18;i++){ const ch=1+Math.floor(Math.random()*5); const t=new Date(now-i*1000*60*(8+Math.random()*40)); events.push({time:t.toLocaleTimeString(),chamber:'Chamber-'+ch,event:'Door Opened', durMin:+(Math.random()*4+0.2).toFixed(1),op:['Operator A','Operator B','Loading Team','QC Team'][Math.floor(Math.random()*4)]}); } events.sort((a,b)=>b.time.localeCompare(a.time)); return events; } state.doorLog = seedDoorLog(); // persistent for the session, used by both the log page and the daily report function renderDoorLog(){ const tb=document.getElementById('doorTbody'); tb.innerHTML=''; state.doorLog.forEach(e=>{tb.innerHTML+=`${e.time}${e.chamber}${e.event}${e.durMin} min${e.op}`;}); } /* occasionally add a fresh door event so the report/log grows during the session */ setInterval(()=>{ if(state.estop)return; if(Math.random()<0.15){ const ch=1+Math.floor(Math.random()*5); state.doorLog.unshift({time:nowStr(),chamber:'Chamber-'+ch,event:'Door Opened', durMin:+(Math.random()*4+0.2).toFixed(1),op:['Operator A','Operator B','Loading Team','QC Team'][Math.floor(Math.random()*4)]}); if(state.doorLog.length>300) state.doorLog.pop(); } },20000); /* ---------------- CALIBRATION ---------------- */ const calibTags=[ {tag:'PT-COMP1-SUCT',param:'Compressor-1 Suction Pressure',lo:0,hi:20,unit:'bar',live:()=>1.1+Math.random()*0.3}, {tag:'PT-COMP1-DISC',param:'Compressor-1 Discharge Pressure',lo:0,hi:25,unit:'bar',live:()=>11+Math.random()*1}, {tag:'TT-COMP1-DISC',param:'Compressor-1 Discharge Temp',lo:-40,hi:150,unit:'°C',live:()=>85+Math.random()*4}, {tag:'PT-LPR-OUT',param:'LPR Pump Outlet Pressure',lo:0,hi:15,unit:'bar',live:()=>3+Math.random()*0.5}, {tag:'TT-LPR-OUT',param:'LPR Pump Outlet Temp',lo:-40,hi:20,unit:'°C',live:()=>-28+Math.random()*2}, {tag:'TT-CH1',param:'Chamber-1 Temperature',lo:-40,hi:20,unit:'°C',live:()=>state.chambers[0].temp}, {tag:'TT-CH2',param:'Chamber-2 Temperature',lo:-40,hi:20,unit:'°C',live:()=>state.chambers[1].temp}, {tag:'HT-CH1',param:'Chamber-1 RH%',lo:0,hi:100,unit:'%',live:()=>state.chambers[0].rh}, {tag:'PT-COND-HEAD',param:'Condenser Head Pressure',lo:0,hi:20,unit:'bar',live:()=>state.cond.headPressure}, ]; function renderCalib(){ const tb=document.getElementById('calibTbody'); tb.innerHTML=''; calibTags.forEach(c=>{ tb.innerHTML+=`${c.tag}${c.param}${c.lo} ${c.unit}${c.hi} ${c.unit} ${fmt(c.live(),2)} ${c.unit} `; }); } /* ---------------- ANALOG SCALING (manual min/max engineering range) ---------------- */ // seeded from the same sensor list used on the Calibration page; kept as its own editable state const analogScaling = {}; calibTags.forEach(c=>{ analogScaling[c.tag] = {min:c.lo, max:c.hi}; }); function scaledLiveValue(c){ const s=analogScaling[c.tag]; const raw=c.live(); // simulated engineering-unit reading // clamp the display to the configured range so an out-of-range reading is visibly flagged return Math.max(s.min, Math.min(s.max, raw)); } function renderScaling(){ const tb=document.getElementById('scalingTbody'); if(!tb)return; tb.innerHTML=''; calibTags.forEach(c=>{ const s=analogScaling[c.tag]; const raw=c.live(); const outOfRange = raws.max; tb.innerHTML+=` ${c.tag}${c.param}${c.unit} ${fmt(raw,2)} ${c.unit}${outOfRange?' ⚠':''} `; }); tb.querySelectorAll('.scale-save').forEach(btn=>btn.addEventListener('click',e=>{ const tr=e.target.closest('tr'); const tag=tr.dataset.scaletag; const minInput=tr.querySelector('.scale-min'), maxInput=tr.querySelector('.scale-max'); const min=parseFloat(minInput.value), max=parseFloat(maxInput.value); if(isNaN(min)||isNaN(max)||min>=max){ minInput.style.borderColor='var(--red)'; maxInput.style.borderColor='var(--red)'; pushAlarm('ANALOG SCALING','Rejected range update for '+tag+' — min must be less than max','warning'); renderAlarms(); return; } minInput.style.borderColor=''; maxInput.style.borderColor=''; analogScaling[tag]={min,max}; pushAlarm('ANALOG SCALING','Range for '+tag+' set to '+min+' – '+max,'info'); renderScaling(); renderAlarms(); })); } document.getElementById('scalingSaveAll').addEventListener('click',()=>{ const tb=document.getElementById('scalingTbody'); let rejected=0; tb.querySelectorAll('tr').forEach(tr=>{ const tag=tr.dataset.scaletag; const min=parseFloat(tr.querySelector('.scale-min').value), max=parseFloat(tr.querySelector('.scale-max').value); if(isNaN(min)||isNaN(max)||min>=max){ rejected++; return; } analogScaling[tag]={min,max}; }); pushAlarm('ANALOG SCALING','Bulk range save — '+(calibTags.length-rejected)+' updated'+(rejected?', '+rejected+' rejected':''),'info'); renderScaling(); renderAlarms(); }); /* ---------------- DI/DO DIAGNOSIS ---------------- */ function renderDiag(){ const tb=document.getElementById('diagTbody'); tb.innerHTML=''; const feeders=[]; state.compressors.forEach(c=>feeders.push({name:'Compressor-'+c.id,auto:true,on:c.running,trip:c.olr})); state.chambers.forEach(ch=>ch.evaps.forEach(ev=>feeders.push({name:`CH${ch.id}-EVAP${ev.id} Fan`,auto:ev.auto,on:ev.running,trip:ev.olr}))); state.lpr.pumps.forEach((p,i)=>feeders.push({name:'LPR Pump-'+(i+1),auto:true,on:p.running,trip:p.olr})); state.cond.pumps.forEach((p,i)=>feeders.push({name:'Cond. Pump-'+(i+1),auto:true,on:p.running,trip:p.olr})); state.cond.fans.forEach((f,i)=>feeders.push({name:'Cond. Fan-'+(i+1),auto:state.cond.autoFan,on:f.running,trip:f.olr})); feeders.forEach(f=>{ tb.innerHTML+=`${f.name}${f.auto?'AUTO':'MANUAL'} ${f.on?'ON':'OFF'} ${f.trip?'TRIP':'— healthy'}`; }); } /* ---------------- CONTROL PANEL GA ---------------- */ function renderPanelGA(){ const g=document.getElementById('panelGA'); if(!g)return; g.innerHTML=''; const feeders=['INCOMER','COMP-1','COMP-2','COMP-3','LPR PUMP-1','LPR PUMP-2','COND PUMP-1','COND PUMP-2','COND FAN-1','COND FAN-2','COND FAN-3','SPARE']; feeders.forEach((f,i)=>{ const x=20+(i%6)*160, y=20+Math.floor(i/6)*140; g.innerHTML+=` ${f} MCCB ${i===0?'800A TPN':'MCCB + Contactor'}`; }); } /* ---------------- MACHINERY RUNTIME LOG ---------------- */ const runtimeSeed={}; function equipRuntimeList(){ const list=[]; state.compressors.forEach(c=>list.push({name:'Compressor-'+c.id+' ('+c.role+')',running:c.running,key:'comp'+c.id})); state.chambers.forEach(ch=>ch.evaps.forEach(ev=>list.push({ name:'Chamber-'+ch.id+' Evaporator-'+ev.id, running:ev.running && ev.fan && !ev.defrost, key:'ch'+ch.id+'evap'+ev.id }))); state.lpr.pumps.forEach((p,i)=>list.push({name:'LPR Pump-'+(i+1),running:p.running,key:'lpr'+i})); state.cond.pumps.forEach((p,i)=>list.push({name:'Condenser Pump-'+(i+1),running:p.running,key:'cpump'+i})); state.cond.fans.forEach((f,i)=>list.push({name:'Condenser Fan-'+(i+1),running:f.running,key:'cfan'+i})); return list; } function renderRuntime(){ const tb=document.getElementById('runtimeTbody'); tb.innerHTML=''; equipRuntimeList().forEach(e=>{ if(!runtimeSeed[e.key]) runtimeSeed[e.key]={today:Math.random()*8,total:2000+Math.random()*8000,lastStart:'06:1'+Math.floor(Math.random()*9)}; const r=runtimeSeed[e.key]; tb.innerHTML+=`${e.name}${e.running?'RUN':'OFF'} ${fmt(r.today,1)}${fmt(r.total,0)}${r.lastStart}`; }); } setInterval(()=>{ // accrue runtime only while the item is actually running equipRuntimeList().forEach(e=>{ const r=runtimeSeed[e.key]; if(r && e.running){ r.today+=1/3600; r.total+=1/3600; } }); },1000); /* ---------------- MACHINERY RUNTIME LOG — DAY-WISE PRINTABLE REPORT ---------------- */ const runtimeDaySel=document.getElementById('runtimeDaySelect'); if(runtimeDaySel) populateDaySelect(runtimeDaySel); function runtimeDataForDay(dk){ return equipRuntimeList().map(e=>{ if(isToday(dk)){ const r=runtimeSeed[e.key]||{today:0,total:0}; const rng=seededRng(dk+'|'+e.key+'|starts'); // starts count isn't separately tracked live, so keep it consistent per day return {name:e.name, running:e.running, hours:r.today, starts:1+Math.floor(rng()*5)}; } const rng=seededRng(dk+'|'+e.key); const hours=+(rng()*22).toFixed(1); const starts=1+Math.floor(rng()*5); return {name:e.name, running:hours>0 && rng()>0.3, hours, starts}; }); } function printRuntimeDayReport(dk){ const rows=runtimeDataForDay(dk); const totalHours=rows.reduce((s,r)=>s+r.hours,0); const label=isToday(dk)?'Today — '+new Date().toLocaleDateString():new Date(dk).toLocaleDateString(); const tableRows=rows.map(r=>`${r.name}${r.running?'RUN (end of day)':'OFF (end of day)'} ${fmt(r.hours,1)}${r.starts}`).join(''); const w=window.open('','_blank'); w.document.write(`Runtime Report ${dk} ${printBrandHeader()}

Machinery Runtime Log

Date: ${label}  |  Generated: ${new Date().toLocaleString()}  |  Plant: Ammonia Cold Storage
${tableRows}
EquipmentStatusRuntime (hrs)Start Count
TOTAL RUN HOURS${fmt(totalHours,1)}
${printBrandFooter()}`); w.document.close(); setTimeout(()=>w.print(),300); } const runtimePrintBtn=document.getElementById('runtimePrintDay'); if(runtimePrintBtn) runtimePrintBtn.addEventListener('click',()=>{ printRuntimeDayReport(runtimeDaySel.value || dateKey(new Date())); }); /* ---------------- ALARMS PAGE ---------------- */ function renderAlarms(){ const list=document.getElementById('alarmList'); if(!list)return; list.innerHTML=''; state.alarms.slice(0,60).forEach(a=>{ const cls = a.severity==='critical'?'crit':a.severity==='warning'?'warn':'info'; const isAlarm = a.severity==='critical' || a.severity==='warning'; const ackCls = isAlarm ? (a.ack?' acked-item':' unacked') : ''; const ackTag = (isAlarm && a.ack) ? 'ACK' : ''; list.innerHTML+=`
${a.time}
${a.tag}
${a.component} — ${a.description}${ackTag}
`; }); document.getElementById('alarmLiveCount').textContent=state.alarms.length+' events'; // "Active" = critical alarms. Fresh (unacknowledged) alarms blink red; // once the operator acknowledges everything the pill turns green. const criticals=state.alarms.filter(a=>a.severity==='critical'); const active=criticals.length; const freshCount=state.alarms.filter(a=>(a.severity==='critical'||a.severity==='warning') && !a.ack).length; document.getElementById('alarmCount').textContent=active; document.getElementById('kpiAlarms').textContent=active; const pill=document.getElementById('alarmPill'); if(pill){ pill.classList.remove('fresh','acked'); if(freshCount>0) pill.classList.add('fresh'); else pill.classList.add('acked'); } const sysDot=document.getElementById('sysDot'), sysText=document.getElementById('sysStatusText'); if(state.estop){sysDot.classList.add('alarm');sysText.textContent='EMERGENCY STOP';} else if(freshCount>0){sysDot.classList.add('alarm');sysText.textContent='ALARM ACTIVE';} else if(active>0){sysDot.classList.remove('alarm');sysText.textContent='ALARMS ACKNOWLEDGED';} else{sysDot.classList.remove('alarm');sysText.textContent='SYSTEM NORMAL';} } document.getElementById('ackAll').addEventListener('click',()=>{ const pending=state.alarms.filter(a=>(a.severity==='critical'||a.severity==='warning') && !a.ack).length; state.alarms.forEach(a=>a.ack=true); // the acknowledgement event itself is informational, so it must not re-trigger the fresh state state.eventSeq++; state.alarms.unshift({id:state.eventSeq,time:nowStr(),tag:'EVT-'+String(state.eventSeq).padStart(4,'0'), component:'SYSTEM',description:'All alarms acknowledged by operator ('+pending+' pending)',severity:'info',ack:true}); renderAll(); }); /* ---------------- BLAST FREEZER ---------------- */ let blastTimer=null; function nextBatchNo(){ return 'BF-'+String(state.batchCounter+1).padStart(4,'0'); } document.getElementById('blastBatchNo').value = nextBatchNo(); function renderBlastChart(){ const svg=document.getElementById('blastChart'); if(!svg)return; const h=state.blast.history; let path=''; h.forEach((v,i)=>{const x=(i/(Math.max(h.length-1,1)))*880+10; const y=200-((v+40)/45)*180; path+=(i===0?'M':'L')+x+','+y+' ';}); svg.innerHTML=` 5°C-40°C`; } document.getElementById('blastStart').addEventListener('click',()=>{ if(state.blast.running)return; let batchNo=(document.getElementById('blastBatchNo').value||'').trim() || nextBatchNo(); state.batchCounter++; const product=(document.getElementById('blastProduct').value||'').trim() || 'Unspecified product'; state.blast.running=true; state.blast.elapsed=0; state.blast.core=5; state.blast.history=[]; const batch={ batchNo, product, chamber:'Blast Freezer-1', startTime:new Date(), startTimeStr:new Date().toLocaleString(), endTime:null, endTimeStr:'--', durationMin:0, startCore:5, endCore:null, status:'RUNNING', stageLog:[{stage:'PULL-DOWN', atMin:0}], fullHistory:[{tMin:0,core:5}] }; state.batchHistory.unshift(batch); state.blast.currentBatch=batch; document.getElementById('blastChartBatch').textContent='batch '+batchNo+' — running'; document.getElementById('blastBatchNo').value=nextBatchNo(); pushAlarm('BLAST FREEZER','Freezing cycle started — Batch '+batchNo+' ('+product+')','info'); blastTimer=setInterval(()=>{ state.blast.elapsed++; if(state.blast.core>-35) state.blast.core -= 0.35+Math.random()*0.1; const prevStage=state.blast.stage; state.blast.stage = state.blast.core>-18?'PULL-DOWN':state.blast.core>-30?'DEEP FREEZE':'HOLD'; state.blast.history.push(state.blast.core); if(state.blast.history.length>120)state.blast.history.shift(); const b=state.blast.currentBatch; if(b){ const tMin=+(state.blast.elapsed*0.4/60).toFixed(2); b.fullHistory.push({tMin,core:state.blast.core}); if(state.blast.stage!==prevStage && state.blast.stage!==b.stageLog[b.stageLog.length-1].stage){ b.stageLog.push({stage:state.blast.stage,atMin:tMin}); } } if(state.blast.core<=-35){ state.blast.stage='COMPLETE'; clearInterval(blastTimer); state.blast.running=false; finalizeBatch('COMPLETE'); pushAlarm('BLAST FREEZER','Freezing cycle complete — Batch '+(b?b.batchNo:''),'info'); } renderBlast(); },400); }); document.getElementById('blastStop').addEventListener('click',()=>{ clearInterval(blastTimer); state.blast.running=false; state.blast.stage='ABORTED'; const b=state.blast.currentBatch; finalizeBatch('ABORTED'); pushAlarm('BLAST FREEZER','Freezing cycle aborted by operator — Batch '+(b?b.batchNo:''),'warning'); renderBlast(); }); function finalizeBatch(status){ const b=state.blast.currentBatch; if(!b)return; b.status=status; b.endTime=new Date(); b.endTimeStr=b.endTime.toLocaleString(); b.durationMin=+((b.endTime-b.startTime)/60000).toFixed(1); b.endCore=state.blast.core; b.stageLog.push({stage:status,atMin:+(state.blast.elapsed*0.4/60).toFixed(2)}); state.blast.currentBatch=null; document.getElementById('blastChartBatch').textContent='no active batch'; renderBatchHistory(); } function renderBlast(){ document.getElementById('blastStage').textContent=state.blast.stage; const m=Math.floor(state.blast.elapsed*0.4/60), s=Math.floor(state.blast.elapsed*0.4%60); document.getElementById('blastElapsed').textContent=pad(m)+':'+pad(s); document.getElementById('blastCore').textContent=fmt(state.blast.core)+' °C'; renderBlastChart(); } function renderBatchHistory(){ const tb=document.getElementById('batchHistoryTbody'); if(!tb)return; tb.innerHTML=''; state.batchHistory.forEach(b=>{ const badge = b.status==='RUNNING'?'on':b.status==='COMPLETE'?'on':b.status==='ABORTED'?'warn':'off'; tb.innerHTML+=`${b.batchNo}${b.product}${b.startTimeStr}${b.endTimeStr} ${b.status==='RUNNING'?'--':b.durationMin+' min'} ${b.status} ${fmt(b.startCore)} → ${b.endCore===null?'--':fmt(b.endCore)}`; }); const sel=document.getElementById('batchSelect'); const prevVal=sel.value; sel.innerHTML = state.batchHistory.length ? state.batchHistory.map(b=>``).join('') : ''; if(state.batchHistory.some(b=>b.batchNo===prevVal)) sel.value=prevVal; } document.getElementById('printBatchBtn').addEventListener('click',()=>{ const batchNo=document.getElementById('batchSelect').value; const b=state.batchHistory.find(x=>x.batchNo===batchNo); if(!b){ alert('No batch selected — run or choose a batch first.'); return; } printBatchReport(b); }); function printBatchReport(b){ // build an inline SVG trend chart from the batch's full temperature history const h=b.fullHistory; let path=''; h.forEach((pt,i)=>{ const x=10+(i/Math.max(h.length-1,1))*780; const y=200-((pt.core+40)/45)*180; path+=(i===0?'M':'L')+x+','+y+' '; }); const chartSvg=` 5°C-40°C `; const stageRows=b.stageLog.map(s=>`${s.stage}${s.atMin.toFixed(1)} min`).join(''); const w=window.open('','_blank'); w.document.write(` Batch Report ${b.batchNo} ${printBrandHeader()}

Blast Freezer Batch Report

Ammonia Refrigeration Plant — Blast Freezer-1

Batch Details

Batch / Lot No.${b.batchNo}Status${b.status}
Product${b.product}Chamber${b.chamber}
Start Time${b.startTimeStr}End Time${b.endTimeStr}
Duration${b.durationMin} minStart / End Core Temp${fmt(b.startCore)}°C → ${b.endCore===null?'--':fmt(b.endCore)}°C

Stage Timeline

${stageRows}
StageReached At

Core Temperature Trend

${chartSvg}

Report generated ${new Date().toLocaleString()} from live simulated cycle data.

${printBrandFooter()}`); w.document.close(); setTimeout(()=>w.print(),300); } /* ---------------- DATA LOGGING PAGE ---------------- */ function pushLog(){ const row={time:nowStr(), ch1:fmt(state.chambers[0].temp),ch2:fmt(state.chambers[1].temp),ch3:fmt(state.chambers[2].temp), ch4:fmt(state.chambers[3].temp),ch5:fmt(state.chambers[4].temp), comp:state.compressors.map(c=>({ id:c.id, suct: c.suctP!==undefined ? c.suctP.toFixed(2) : '--', oil: c.oilP !==undefined ? c.oilP.toFixed(2) : '--', disc: c.discP!==undefined ? c.discP.toFixed(2) : '--' }))}; // keep the legacy single-compressor fields so existing exports stay valid row.suct=row.comp[0].suct; row.disc=row.comp[0].disc; state.logBuffer.unshift(row); if(state.logBuffer.length>25) state.logBuffer.pop(); } let compTrendSelected=1; function renderDataLog(){ const tb=document.getElementById('logTbody'); if(!tb)return; tb.innerHTML=''; state.logBuffer.forEach(r=>{tb.innerHTML+=`${r.time}${r.ch1}${r.ch2}${r.ch3}${r.ch4}${r.ch5}`;}); document.getElementById('logCount').textContent=state.logBuffer.length; // compressor pressure table — suction / oil / discharge for all three units const ctb=document.getElementById('logCompTbody'); if(ctb){ ctb.innerHTML=''; state.logBuffer.forEach(r=>{ const cells=(r.comp||[]).map(c=>`${c.suct}${c.oil}${c.disc}`).join(''); ctb.innerHTML+=`${r.time}${cells}`; }); } renderCompChart(); // chart of chamber temps const svg=document.getElementById('dataChart'); if(svg){ const series=[[],[],[],[],[]]; [...state.logBuffer].reverse().forEach(r=>{series[0].push(+r.ch1);series[1].push(+r.ch2);series[2].push(+r.ch3);series[3].push(+r.ch4);series[4].push(+r.ch5);}); const colors=['#2dd4e8','#3ddc84','#ffb020','#a78bfa','#ff6b6b']; let html=''; series.forEach((s,idx)=>{ let path=''; s.forEach((v,i)=>{const x=10+(i/(Math.max(s.length-1,1)))*870; const y=180-((v+30)/35)*160; path+=(i===0?'M':'L')+x+','+y+' ';}); html+=``; }); svg.innerHTML=html; } } function renderCompChart(){ const svg=document.getElementById('compChart'); if(!svg)return; const rows=[...state.logBuffer].reverse(); const idx=compTrendSelected-1; const suct=[],oil=[],disc=[]; rows.forEach(r=>{ const c=(r.comp||[])[idx]; if(!c)return; suct.push(parseFloat(c.suct)||0); oil.push(parseFloat(c.oil)||0); disc.push(parseFloat(c.disc)||0); }); const all=[...suct,...oil,...disc]; const max=Math.max(16,...all), min=0; const plot=(arr,color)=>{ let p=''; arr.forEach((v,i)=>{const x=10+(i/Math.max(arr.length-1,1))*870; const y=180-((v-min)/(max-min))*160; p+=(i===0?'M':'L')+x+','+y+' ';}); return ``; }; svg.innerHTML='' + `${max.toFixed(0)} bar0` + plot(suct,'#2dd4e8') + plot(oil,'#ffb020') + plot(disc,'#ff6b6b'); } const compTrendSelEl=document.getElementById('compTrendSel'); if(compTrendSelEl) compTrendSelEl.querySelectorAll('span').forEach(s=>s.addEventListener('click',()=>{ compTrendSelected=parseInt(s.dataset.v); compTrendSelEl.querySelectorAll('span').forEach(x=>x.classList.toggle('active',x.dataset.v===s.dataset.v)); renderCompChart(); })); document.getElementById('exportCsv').addEventListener('click',()=>{ let csv='Time,Ch1,Ch2,Ch3,Ch4,Ch5,' +'C1_Suct,C1_Oil,C1_Disch,C2_Suct,C2_Oil,C2_Disch,C3_Suct,C3_Oil,C3_Disch\n'; state.logBuffer.forEach(r=>{ const c=(r.comp||[]).map(x=>`${x.suct},${x.oil},${x.disc}`).join(','); csv+=`${r.time},${r.ch1},${r.ch2},${r.ch3},${r.ch4},${r.ch5},${c}\n`; }); const blob=new Blob([csv],{type:'text/csv'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='scada_log.csv'; a.click(); }); document.getElementById('exportPdf').addEventListener('click',()=>{ const w=window.open('','_blank'); w.document.write(`Data Log Export ${printBrandHeader()}

Data Logging Export

Generated: ${new Date().toLocaleString()}

Chamber Temperatures

${document.getElementById('logTbody').closest('table').outerHTML}

Compressor Pressure Parameters (bar)

${document.getElementById('logCompTbody').closest('table').outerHTML} ${printBrandFooter()}`); w.document.close(); setTimeout(()=>w.print(),300); }); /* ---------------- SYSTEM SETTINGS ---------------- */ function renderSettings(){ const cs=document.getElementById('chamberSettings'); if(cs && !cs.dataset.built){ cs.dataset.built='1'; state.chambers.forEach(ch=>{ cs.innerHTML+=`
Chamber-${ch.id} Set Point °C   Δ°C
`; }); cs.querySelectorAll('[data-chsp]').forEach(inp=>inp.addEventListener('change',()=>{ const ch=state.chambers.find(c=>c.id==inp.dataset.chsp); const v=parseFloat(inp.value); if(isNaN(v))return; ch.setpoint=v; pushAlarm('CH'+ch.id,'Temperature set point changed to '+v+'°C','info'); renderAlarms(); })); cs.querySelectorAll('[data-chdelta]').forEach(inp=>inp.addEventListener('change',()=>{ const ch=state.chambers.find(c=>c.id==inp.dataset.chdelta); const v=parseFloat(inp.value); if(isNaN(v)||v<=0)return; ch.delta=v; pushAlarm('CH'+ch.id,'Temperature differential (Δ) changed to '+v+'°C','info'); renderAlarms(); })); } renderPistonSettings(); renderSeqTimerSettings(); renderDefrostSettings(); } function stripTags(s){ return s.replace(/→/g,'→').replace(/≥/g,'≥').replace(/&/g,'&'); } function renderDefrostSettings(){ const btn=document.getElementById('defrostSettingsSave'); if(!btn || btn.dataset.built) return; btn.dataset.built='1'; document.getElementById('defrostTempInput').value=state.settings.defrostTerminationTemp; document.getElementById('defrostTimeInput').value=state.settings.defrostMaxTime; document.getElementById('defrostDripInput').value=state.settings.defrostDripDelay; document.getElementById('defrostPumpdownInput').value=state.settings.defrostPumpdownDelay; document.getElementById('defrostFanOffInput').value=state.settings.defrostFanOffDelay; btn.addEventListener('click',()=>{ const t=parseFloat(document.getElementById('defrostTempInput').value); const m=parseFloat(document.getElementById('defrostTimeInput').value); const dr=parseFloat(document.getElementById('defrostDripInput').value); const pd=parseFloat(document.getElementById('defrostPumpdownInput').value); const fo=parseFloat(document.getElementById('defrostFanOffInput').value); if([t,m,dr,pd,fo].some(isNaN) || m<=0 || dr<0 || pd<0 || fo<0 || fo>pd){ pushAlarm('SYSTEM SETTINGS','Rejected defrost parameter update — invalid value(s); fan OFF delay must not exceed pump-down delay','warning'); renderAlarms(); return; } state.settings.defrostTerminationTemp=t; state.settings.defrostMaxTime=m; state.settings.defrostDripDelay=dr; state.settings.defrostPumpdownDelay=pd; state.settings.defrostFanOffDelay=fo; pushAlarm('SYSTEM SETTINGS','Defrost parameters updated: terminate at '+t+'°C, max '+m+' min, drip '+dr+' min, pump-down '+pd+' s, fan off '+fo+' s','info'); renderAlarms(); }); } function renderSeqTimerSettings(){ const startWrap=document.getElementById('startDelaySettings'), stopWrap=document.getElementById('stopDelaySettings'); if(!startWrap || startWrap.dataset.built) { return; } startWrap.dataset.built='1'; PLANT_START_STEPS.forEach((s,i)=>{ startWrap.innerHTML+=`
${i+1}. ${stripTags(s.label)} sec
`; }); PLANT_STOP_STEPS.forEach((s,i)=>{ stopWrap.innerHTML+=`
${i+1}. ${stripTags(s.label)} sec
`; }); document.getElementById('unloadStepDelayInput').value=state.settings.compressorUnloadStepDelay; document.getElementById('dpsOnDelayInput').value=state.settings.dpsOnDelay; document.getElementById('seqTimersSave').addEventListener('click',()=>{ const newStart=[...document.querySelectorAll('.startdelay-in')].map(i=>parseFloat(i.value)); const newStop=[...document.querySelectorAll('.stopdelay-in')].map(i=>parseFloat(i.value)); const unloadDelay=parseFloat(document.getElementById('unloadStepDelayInput').value); const dpsDelay=parseFloat(document.getElementById('dpsOnDelayInput').value); if(newStart.some(isNaN) || newStop.some(isNaN) || isNaN(unloadDelay) || isNaN(dpsDelay) || unloadDelay<0 || dpsDelay<0){ pushAlarm('SYSTEM SETTINGS','Rejected sequence timer update — invalid delay value(s)','warning'); renderAlarms(); return; } state.settings.startDelays=newStart; state.settings.stopDelays=newStop; state.settings.compressorUnloadStepDelay=unloadDelay; state.settings.dpsOnDelay=dpsDelay; pushAlarm('SYSTEM SETTINGS','Plant sequence timers updated by operator','info'); renderAlarms(); }); } function renderPistonSettings(){ const wrap=document.getElementById('pistonSettings'); if(!wrap)return; wrap.innerHTML=''; state.compressors.forEach(c=>{ const block=document.createElement('div'); block.style.marginBottom='14px'; block.innerHTML=`
COMPRESSOR-${c.id}
PistonLoad Pressure (bar)Unload Pressure (bar)ON Delay (sec)OFF Delay (sec)Action
`; wrap.appendChild(block); const tb=block.querySelector('[data-pistontbody]'); c.pistons.forEach(p=>{ const tr=document.createElement('tr'); tr.innerHTML=`Piston-${p.id} `; tb.appendChild(tr); tr.querySelector('.p-save').addEventListener('click',()=>{ const loadP=parseFloat(tr.querySelector('.p-load').value); const unloadP=parseFloat(tr.querySelector('.p-unload').value); const onDelay=parseFloat(tr.querySelector('.p-ondelay').value); const offDelay=parseFloat(tr.querySelector('.p-offdelay').value); if([loadP,unloadP,onDelay,offDelay].some(isNaN) || unloadP>=loadP){ pushAlarm('COMP-'+c.id,'Piston-'+p.id+' setting rejected — unload pressure must be less than load pressure','warning'); renderAlarms(); return; } p.loadP=loadP; p.unloadP=unloadP; p.onDelay=onDelay; p.offDelay=offDelay; pushAlarm('COMP-'+c.id,'Piston-'+p.id+' settings updated: Load '+loadP+' / Unload '+unloadP+' bar, ON '+onDelay+'s / OFF '+offDelay+'s','info'); renderAlarms(); }); }); }); } /* ---------------- KPIs ---------------- */ function renderKpis(){ const running=state.compressors.filter(c=>c.running).length; document.getElementById('kpiRunning').textContent=running+'/3'; const avgLoad=state.compressors.reduce((s,c)=>s+(c.running?c.load:0),0)/3; document.getElementById('kpiLoad').textContent=fmt(avgLoad,0)+'%'; } /* ---------------- DAILY SUMMARY REPORT ---------------- */ function fmtDuration(hrs){ const h=Math.floor(hrs), m=Math.round((hrs-h)*60); return h+'h '+String(m).padStart(2,'0')+'m'; } function reportWindowHours(){ return Math.max((Date.now()-state.sessionStart)/3600000, 1/3600); } function renderReport(){ const rd=document.getElementById('repDate'); if(!rd) return; // page not yet in DOM on very first call is fine, elements always exist const d=new Date(); document.getElementById('repDate').textContent=d.toLocaleDateString(); document.getElementById('repGenTime').textContent=d.toLocaleTimeString(); document.getElementById('repWindow').textContent=new Date(state.sessionStart).toLocaleTimeString()+' → '+d.toLocaleTimeString(); /* --- 1) Energy Consumption Report --- */ const etb=document.getElementById('repEnergyTbody'); etb.innerHTML=''; let totalKwh=0; const equipNow=equipmentList(); Object.keys(state.dailyEnergyKwh).forEach(tag=>{ const acc=state.dailyEnergyKwh[tag]; const nowEquip=equipNow.find(e=>e.tag===tag); const avgKw=acc.samples? acc.sampleKw/acc.samples : 0; totalKwh+=acc.kwh; etb.innerHTML+=`${tag}${nowEquip&&nowEquip.running?'RUN':'OFF'} ${fmt(avgKw)}${fmt(acc.kwh,2)}₹${fmt(acc.kwh*8,0)}`; }); document.getElementById('repKpiEnergy').textContent=fmt(totalKwh,1)+' kWh'; /* --- 2) Alarm Report --- */ const atb=document.getElementById('repAlarmTbody'); atb.innerHTML=''; const crit=state.alarms.filter(a=>a.severity==='critical').length; const warn=state.alarms.filter(a=>a.severity==='warning').length; const info=state.alarms.filter(a=>a.severity==='info').length; document.getElementById('repAlarmCrit').textContent=crit; document.getElementById('repAlarmWarn').textContent=warn; document.getElementById('repAlarmInfo').textContent=info; document.getElementById('repKpiAlarms').textContent=state.alarms.length; document.getElementById('repKpiCritical').textContent=crit; state.alarms.slice(0,80).forEach(a=>{ atb.innerHTML+=`${a.time}${a.tag}${a.component}${a.description} ${a.severity.toUpperCase()}`; }); /* --- 3) Temperature Log Summary --- */ const ttb=document.getElementById('repTempTbody'); ttb.innerHTML=''; state.chambers.forEach(ch=>{ const s=state.chamberDailyStats[ch.id] || {min:ch.temp,max:ch.temp,sum:ch.temp,count:1}; const avg=s.count? s.sum/s.count : ch.temp; ttb.innerHTML+=`Chamber-${ch.id}${fmt(s.min===Infinity?ch.temp:s.min)}${fmt(s.max===-Infinity?ch.temp:s.max)} ${fmt(avg)}${fmt(ch.temp)}${fmt(ch.rh,0)}`; }); /* --- 4) Door Opening Log Summary --- */ const dstb=document.getElementById('repDoorSummaryTbody'); dstb.innerHTML=''; const byChamber={}; state.doorLog.forEach(e=>{ if(!byChamber[e.chamber]) byChamber[e.chamber]={count:0,mins:0}; byChamber[e.chamber].count++; byChamber[e.chamber].mins+=e.durMin; }); Object.keys(byChamber).sort().forEach(ch=>{ const b=byChamber[ch]; dstb.innerHTML+=`${ch}${b.count}${fmtDuration(b.mins/60)}`; }); document.getElementById('repKpiDoors').textContent=state.doorLog.length; const ddtb=document.getElementById('repDoorDetailTbody'); ddtb.innerHTML=''; state.doorLog.slice(0,50).forEach(e=>{ ddtb.innerHTML+=`${e.time}${e.chamber}${e.event}${e.durMin} min${e.op}`; }); /* --- 5) Machinery-wise Daily Running Hours --- */ const rhtb=document.getElementById('repRunHoursTbody'); rhtb.innerHTML=''; const elapsedHrs=Math.max((Date.now()-state.sessionStart)/3600000, 1/3600); let totalRunHrs=0; equipRuntimeList().forEach(e=>{ const r=runtimeSeed[e.key]; if(!r) return; totalRunHrs+=r.today; const util=Math.min(100,(r.today/elapsedHrs)*100); rhtb.innerHTML+=`${e.name} ${e.running?'RUN':'OFF'} ${fmtDuration(r.today)}${fmt(r.total,0)}${fmt(util,0)}%`; }); document.getElementById('repKpiRunHours').textContent=fmt(totalRunHrs,1)+' h'; } document.getElementById('reportGenerate').addEventListener('click',renderReport); document.getElementById('reportPrint').addEventListener('click',()=>{ renderReport(); const w=window.open('','_blank'); const d=new Date(); w.document.write(` Daily Summary Report ${d.toLocaleDateString()} ${printBrandHeader()}

Ammonia Refrigeration Plant — Daily Summary Report

Date: ${d.toLocaleDateString()}  |  Generated: ${d.toLocaleTimeString()}  |  Window: ${document.getElementById('repWindow').textContent}

1) Energy Consumption Report

${document.getElementById('repEnergyTbody').closest('table').outerHTML}

2) Alarm Report (Critical: ${document.getElementById('repAlarmCrit').textContent}, Warning: ${document.getElementById('repAlarmWarn').textContent}, Info: ${document.getElementById('repAlarmInfo').textContent})

${document.getElementById('repAlarmTbody').closest('table').outerHTML}

3) Temperature Log Summary

${document.getElementById('repTempTbody').closest('table').outerHTML}

4) Door Opening Log Summary

${document.getElementById('repDoorSummaryTbody').closest('table').outerHTML}

Door Opening Log — Detail

${document.getElementById('repDoorDetailTbody').closest('table').outerHTML}

5) Machinery-wise Daily Running Hours

${document.getElementById('repRunHoursTbody').closest('table').outerHTML} ${printBrandFooter()}`); w.document.close(); setTimeout(()=>w.print(),300); }); document.getElementById('reportCsv').addEventListener('click',()=>{ renderReport(); let csv='AMMONIA REFRIGERATION PLANT - DAILY SUMMARY REPORT\n'; csv+='Date,'+new Date().toLocaleDateString()+'\nGenerated,'+new Date().toLocaleTimeString()+'\n\n'; csv+='1) ENERGY CONSUMPTION REPORT\nEquipment Tag,Status,Avg Power (kW),Energy Today (kWh),Est. Cost\n'; Object.keys(state.dailyEnergyKwh).forEach(tag=>{ const acc=state.dailyEnergyKwh[tag]; const nowEquip=equipmentList().find(e=>e.tag===tag); const avgKw=acc.samples? acc.sampleKw/acc.samples : 0; csv+=`${tag},${nowEquip&&nowEquip.running?'RUN':'OFF'},${fmt(avgKw)},${fmt(acc.kwh,2)},${fmt(acc.kwh*8,0)}\n`; }); csv+='\n2) ALARM REPORT\nTime,Tag,Component,Description,Severity\n'; state.alarms.forEach(a=>{csv+=`${a.time},${a.tag},${a.component},"${a.description}",${a.severity}\n`;}); csv+='\n3) TEMPERATURE LOG SUMMARY\nChamber,Min,Max,Avg,Current,RH%\n'; state.chambers.forEach(ch=>{ const s=state.chamberDailyStats[ch.id]||{min:ch.temp,max:ch.temp,sum:ch.temp,count:1}; csv+=`Chamber-${ch.id},${fmt(s.min===Infinity?ch.temp:s.min)},${fmt(s.max===-Infinity?ch.temp:s.max)},${fmt(s.sum/s.count)},${fmt(ch.temp)},${fmt(ch.rh,0)}\n`; }); csv+='\n4) DOOR OPENING LOG\nTime,Chamber,Event,Duration (min),Operator\n'; state.doorLog.forEach(e=>{csv+=`${e.time},${e.chamber},${e.event},${e.durMin},${e.op}\n`;}); csv+='\n5) MACHINERY-WISE DAILY RUNNING HOURS\nEquipment,Status,Running Hours Today,Total Runtime (hrs)\n'; equipRuntimeList().forEach(e=>{ const r=runtimeSeed[e.key]; if(!r) return; csv+=`${e.name},${e.running?'RUN':'OFF'},${fmt(r.today,2)},${fmt(r.total,0)}\n`; }); const blob=new Blob([csv],{type:'text/csv'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='daily_summary_report_'+new Date().toISOString().slice(0,10)+'.csv'; a.click(); }); /* ---------------- QR ACCESS PAGE ---------------- */ let qrCurrentUrl=''; function renderQr(url){ const svg=document.getElementById('qrSvg'); const meta=document.getElementById('qrMeta'); const echo=document.getElementById('qrUrlEcho'); if(!svg) return; if(!url){ svg.innerHTML='Enter a URL and press Generate'; meta.textContent='—'; echo.textContent=''; return; } let q; try{ q=QR.encode(url); } catch(err){ svg.innerHTML='URL too long for a QR code'; meta.textContent='error'; return; } qrCurrentUrl=url; const quiet=4, dim=q.size+quiet*2; let rects=''; for(let y=0;y`; } } // centre logo — level H tolerates roughly 30% loss; keep the patch well under that const logoW=dim*0.26, logoH=logoW*(296/838), lx=(dim-logoW)/2, ly=(dim-logoH)/2; const pad=logoW*0.06; svg.setAttribute('viewBox','0 0 '+dim+' '+dim); svg.innerHTML=`${rects} `; meta.textContent='v'+q.version+' · mask '+q.mask+' · ECC-H · '+q.size+'×'+q.size; echo.textContent=url; } const qrGenBtn=document.getElementById('qrGenerate'); if(qrGenBtn){ qrGenBtn.addEventListener('click',()=>{ const v=document.getElementById('qrUrlInput').value.trim(); renderQr(v); if(v) pushAlarm('QR ACCESS','Access QR generated for '+v,'info'), renderAlarms(); }); document.getElementById('qrUseCurrent').addEventListener('click',()=>{ const u=window.location.href; document.getElementById('qrUrlInput').value=u; renderQr(u); }); document.getElementById('qrPrint').addEventListener('click',()=>{ if(!qrCurrentUrl){ alert('Generate a QR code first.'); return; } const svgHtml=document.getElementById('qrSvg').outerHTML; const w=window.open('','_blank'); w.document.write(`SCADA Access QR ${printBrandHeader()}

Ammonia Refrigeration SCADA — Access QR

Scan to open the plant SCADA interface
${svgHtml}
${qrCurrentUrl}
${printBrandFooter()}`); w.document.close(); setTimeout(()=>w.print(),400); }); // prefill with the current location when the file is served over http(s) if(/^https?:/.test(window.location.href)){ document.getElementById('qrUrlInput').value=window.location.href; renderQr(window.location.href); } else { renderQr(''); } } /* ---------------- CASUAL COPY DETERRENTS ---------------- NOTE: these only discourage casual copying. Any page a browser can render can also be saved. Real access control has to be enforced by the web server. */ document.addEventListener('contextmenu',e=>e.preventDefault()); document.addEventListener('keydown',e=>{ const k=(e.key||'').toLowerCase(); const block = (e.ctrlKey||e.metaKey) && ['s','u','p'].includes(k); const devtools = k==='f12' || ((e.ctrlKey||e.metaKey) && e.shiftKey && ['i','j','c'].includes(k)); if(block || devtools){ e.preventDefault(); pushAlarm('SECURITY','Blocked save/view-source/devtools shortcut','warning'); renderAlarms(); return false; } }); /* ================= ACCESS CONTROL ================= Three levels: OPERATOR (default, no password) < SUPERVISOR < ENGINEER. Protected pages and critical actions require the matching level. NOTE: this is an operator-discipline control implemented in the browser — it stops the wrong person changing a set point, but it is not a security boundary. Real plant access control belongs in the PLC/HMI and on the server. */ const ACCESS = { level:'OPERATOR', order:{OPERATOR:0, SUPERVISOR:1, ENGINEER:2}, autoLockMin:10, lastActivity:Date.now(), pw:{ SUPERVISOR:'2468', ENGINEER:'1357' }, // change from System Settings pending:null }; const PAGE_ACCESS = { settings:'ENGINEER', calib:'ENGINEER', scaling:'ENGINEER', diag:'SUPERVISOR', qr:'ENGINEER', panel:'SUPERVISOR', maint:'SUPERVISOR' }; const ACTION_ACCESS = { 'estop-reset':'SUPERVISOR', 'plant-start':'SUPERVISOR', 'plant-stop':'SUPERVISOR', 'nh3-threshold':'ENGINEER', 'defrost-params':'ENGINEER', 'seq-timers':'ENGINEER', 'piston-settings':'ENGINEER', 'chamber-setpoint':'SUPERVISOR', 'compressor-role':'SUPERVISOR', 'scaling-save':'ENGINEER', 'change-password':'ENGINEER' }; function hasAccess(lvl){ return ACCESS.order[ACCESS.level] >= ACCESS.order[lvl]; } function touchActivity(){ ACCESS.lastActivity = Date.now(); } ['click','keydown','mousemove'].forEach(ev=>document.addEventListener(ev, touchActivity, {passive:true})); /* auto-lock on inactivity */ setInterval(()=>{ if(ACCESS.level==='OPERATOR') return; if((Date.now()-ACCESS.lastActivity)/60000 >= ACCESS.autoLockMin){ const was = ACCESS.level; ACCESS.level='OPERATOR'; pushAlarm('SECURITY','Session auto-locked after '+ACCESS.autoLockMin+' min inactivity (was '+was+')','info'); renderAccess(); renderAlarms(); enforcePageAccess(); } },15000); function renderAccess(){ const pill=document.getElementById('accPill'), txt=document.getElementById('accLevelTxt'); if(!pill) return; pill.classList.remove('op','sup','eng'); pill.classList.add(ACCESS.level==='ENGINEER'?'eng':ACCESS.level==='SUPERVISOR'?'sup':'op'); pill.firstChild.textContent = ACCESS.level==='OPERATOR' ? '🔒 ' : '🔓 '; txt.textContent = ACCESS.level; document.querySelectorAll('.nav-item[data-page]').forEach(it=>{ const need = PAGE_ACCESS[it.dataset.page]; it.classList.toggle('protected', !!need); it.classList.toggle('locked-nav', !!need && !hasAccess(need)); let ic = it.querySelector('.lockicon'); if(need){ if(!ic){ ic=document.createElement('span'); ic.className='lockicon'; it.appendChild(ic); } ic.textContent = hasAccess(need) ? '🔓' : '🔒'; } else if(ic){ ic.remove(); } }); } /* password prompt — returns via callback */ function requireAccess(level, reason, onGranted){ if(hasAccess(level)){ onGranted(); return; } ACCESS.pending = {level, reason, onGranted}; const bg=document.getElementById('authBg'); document.getElementById('authTitle').textContent = level+' ACCESS REQUIRED'; document.getElementById('authSub').textContent = reason || 'Enter the '+level.toLowerCase()+' password to continue'; document.getElementById('authHint').textContent = 'Signed in as '+ACCESS.level+'. Session auto-locks after '+ACCESS.autoLockMin+' minutes idle.'; document.getElementById('authErr').style.display='none'; const inp=document.getElementById('authInput'); inp.value=''; bg.classList.add('show'); setTimeout(()=>inp.focus(),50); } function closeAuth(){ document.getElementById('authBg').classList.remove('show'); ACCESS.pending=null; } function submitAuth(){ const p=ACCESS.pending; if(!p) return; const val=document.getElementById('authInput').value; const need=p.level; let ok=false, gained=null; if(val===ACCESS.pw.ENGINEER){ ok=true; gained='ENGINEER'; } else if(val===ACCESS.pw.SUPERVISOR && ACCESS.order[need]<=ACCESS.order.SUPERVISOR){ ok=true; gained='SUPERVISOR'; } if(!ok){ const e=document.getElementById('authErr'); e.textContent='Incorrect password.'; e.style.display='block'; document.getElementById('authInput').value=''; pushAlarm('SECURITY','Failed '+need+' access attempt — '+(p.reason||''),'warning'); renderAlarms(); return; } if(ACCESS.order[gained] > ACCESS.order[ACCESS.level]) ACCESS.level = gained; touchActivity(); pushAlarm('SECURITY',gained+' access granted — '+(p.reason||'restricted function'),'info'); const cb=p.onGranted; closeAuth(); renderAccess(); renderAlarms(); enforcePageAccess(); if(cb) cb(); } document.getElementById('authOk').addEventListener('click',submitAuth); document.getElementById('authCancel').addEventListener('click',closeAuth); document.getElementById('authInput').addEventListener('keydown',e=>{ if(e.key==='Enter') submitAuth(); }); document.getElementById('authBg').addEventListener('click',e=>{ if(e.target.id==='authBg') closeAuth(); }); document.getElementById('accPill').addEventListener('click',()=>{ if(ACCESS.level!=='OPERATOR'){ const was=ACCESS.level; ACCESS.level='OPERATOR'; pushAlarm('SECURITY','Session locked by operator (was '+was+')','info'); renderAccess(); renderAlarms(); enforcePageAccess(); } else { requireAccess('SUPERVISOR','Sign in to raise access level',()=>{}); } }); /* hide protected page content when the level is insufficient */ function enforcePageAccess(){ Object.keys(PAGE_ACCESS).forEach(pg=>{ const el=document.getElementById('page-'+pg); if(!el) return; let ov=el.querySelector(':scope > .lock-screen'); const need=PAGE_ACCESS[pg]; if(!hasAccess(need)){ Array.from(el.children).forEach(c=>{ if(!c.classList.contains('lock-screen')) c.style.display='none'; }); if(!ov){ ov=document.createElement('div'); ov.className='lock-screen'; ov.innerHTML=`
🔒
${need} ACCESS REQUIRED
This page contains settings that affect plant operation.
`; el.appendChild(ov); ov.querySelector('[data-unlock]').addEventListener('click',()=> requireAccess(need,'Open '+pg+' page',()=>{})); } ov.style.display='flex'; } else { Array.from(el.children).forEach(c=>{ if(!c.classList.contains('lock-screen')) c.style.display=''; }); if(ov) ov.style.display='none'; } }); } /* wrap a click handler so it needs a given access level */ function guard(el, action, reason){ if(!el) return; const lvl=ACTION_ACCESS[action]; el.addEventListener('click',e=>{ if(hasAccess(lvl)) return; e.preventDefault(); e.stopImmediatePropagation(); requireAccess(lvl, reason, ()=>el.click()); }, true); } /* ---- access settings handlers ---- */ function renderAccessSettings(){ const cur=document.getElementById('accCurLevel'); if(!cur) return; cur.textContent=ACCESS.level; const lockInp=document.getElementById('accAutoLock'); if(lockInp && !lockInp.dataset.built){ lockInp.dataset.built='1'; lockInp.value=ACCESS.autoLockMin; document.getElementById('accSaveLock').addEventListener('click',()=>{ const v=parseInt(lockInp.value); if(isNaN(v)||v<1||v>120){ pushAlarm('SECURITY','Rejected auto-lock value','warning'); renderAlarms(); return; } ACCESS.autoLockMin=v; pushAlarm('SECURITY','Auto-lock timeout set to '+v+' minutes','info'); renderAlarms(); }); document.getElementById('accPwSave').addEventListener('click',()=>{ requireAccess(ACTION_ACCESS['change-password'],'Change access password',()=>{ const role=document.getElementById('accPwRole').value; const a=document.getElementById('accPwNew').value, b=document.getElementById('accPwNew2').value; if(a.length<4){ pushAlarm('SECURITY','Password rejected — minimum 4 characters','warning'); renderAlarms(); return; } if(a!==b){ pushAlarm('SECURITY','Password rejected — entries do not match','warning'); renderAlarms(); return; } ACCESS.pw[role]=a; document.getElementById('accPwNew').value=''; document.getElementById('accPwNew2').value=''; pushAlarm('SECURITY',role+' password changed by engineer','info'); renderAlarms(); }); }); } } /* ---- attach guards to critical controls ---- */ function attachGuards(){ if(attachGuards.done) return; const map=[ ['resetBtn','estop-reset','Reset emergency stop'], ['plantStartBtn','plant-start','Start plant master sequence'], ['plantStopBtn','plant-stop','Stop plant master sequence'], ['thSave','nh3-threshold','Change NH3 safety thresholds'], ['defrostSettingsSave','defrost-params','Change defrost parameters'], ['seqTimersSave','seq-timers','Change sequence timers'], ['scalingSaveAll','scaling-save','Bulk save analog scaling ranges'] ]; map.forEach(([id,act,reason])=>guard(document.getElementById(id),act,reason)); attachGuards.done=true; } /* compressor duty-role selectors and chamber set points are re-rendered often, so they are guarded through a delegated capture listener instead */ document.addEventListener('click',e=>{ const roleSpan=e.target.closest('[data-role] span'); if(roleSpan && !hasAccess(ACTION_ACCESS['compressor-role'])){ e.preventDefault(); e.stopImmediatePropagation(); requireAccess(ACTION_ACCESS['compressor-role'],'Change compressor duty role',()=>roleSpan.click()); return; } const pistonSave=e.target.closest('.p-save'); if(pistonSave && !hasAccess(ACTION_ACCESS['piston-settings'])){ e.preventDefault(); e.stopImmediatePropagation(); requireAccess(ACTION_ACCESS['piston-settings'],'Change piston load/unload settings',()=>pistonSave.click()); } },true); document.addEventListener('change',e=>{ if((e.target.dataset&&(e.target.dataset.chsp||e.target.dataset.chdelta)) && !hasAccess(ACTION_ACCESS['chamber-setpoint'])){ e.stopImmediatePropagation(); const el=e.target, prev=el.defaultValue; el.value=prev; requireAccess(ACTION_ACCESS['chamber-setpoint'],'Change chamber temperature set point',()=>{}); } },true); /* ---------------- MASTER RENDER ---------------- */ function renderAll(){ renderCompressors(); renderChambers(); renderLpr(); renderCond(); renderEnergy(); renderKpis(); renderAlarms(); renderFloorPlan(); renderPidMimic(); renderRuntime(); renderDiag(); renderPlantSeq(); if(typeof renderAccess==='function'){ renderAccess(); renderAccessSettings(); attachGuards(); } } applyBranding(); renderAll(); renderAccess(); enforcePageAccess(); attachGuards(); renderAccessSettings(); renderAmmonia(); renderMaint(); renderDoorLog(); renderCalib(); renderScaling(); renderPanelGA(); renderBlast(); renderBatchHistory(); renderSettings(); setInterval(renderAll,2000); setInterval(renderAmmonia,3000); setInterval(()=>{pushLog();renderDataLog();},2000); setInterval(updateDailyAccumulators,2000); renderReport(); setInterval(renderReport,5000);