@@ -0,0 +1,926 @@
// Local simulator that serves the exported build and replaces CGI placeholders from mocks/.
// - Serves the exported "out" folder.
// - Replaces placeholders like <%=SAV00%>, <%=SAN01%>, DANxx/DASxx etc. using mocks/device-cgi-simulator/**.
// - Supports mapping /CPL?/path to /path in the export.
import http from "http" ;
import fs from "fs" ;
import path from "path" ;
const PORT = process . env . PORT ? Number ( process . env . PORT ) : 3030 ;
const ROOT = path . join ( process . cwd ( ) , "out" ) ;
// Simple in-memory caches to speed up local dev
const textCache = new Map ( ) ; // key: absolute path, value: { mtimeMs, body, contentType }
let messagesAllCache = null ; // cache parsed meldungen/messages_all.json
// Helper to read JSON body from POST requests
function readRequestBody ( req ) {
return new Promise ( ( resolve , reject ) => {
let data = "" ;
req . on ( "data" , ( chunk ) => {
data += chunk ;
// Basic guard against huge bodies in local dev
if ( data . length > 1_000_000 ) {
req . destroy ( ) ;
reject ( new Error ( "Request body too large" ) ) ;
}
} ) ;
req . on ( "end" , ( ) => {
try {
const json = data ? JSON . parse ( data ) : { } ;
resolve ( json ) ;
} catch {
resolve ( { } ) ;
}
} ) ;
req . on ( "error" , reject ) ;
} ) ;
}
function exists ( p ) {
try {
fs . accessSync ( p , fs . constants . F _OK ) ;
return true ;
} catch {
return false ;
}
}
function contentTypeByExt ( ext ) {
switch ( ext ) {
case ".html" :
return "text/html; charset=utf-8" ;
case ".js" :
return "application/javascript; charset=utf-8" ;
case ".json" :
return "application/json; charset=utf-8" ;
case ".css" :
return "text/css; charset=utf-8" ;
case ".txt" :
return "text/plain; charset=utf-8" ;
case ".svg" :
return "image/svg+xml" ;
case ".png" :
return "image/png" ;
case ".jpg" :
case ".jpeg" :
return "image/jpeg" ;
case ".ico" :
return "image/x-icon" ;
default :
return "application/octet-stream" ;
}
}
function isTextExt ( ext ) {
return [ ".html" , ".js" , ".json" , ".css" , ".txt" , ".svg" ] . includes ( ext ) ;
}
function readJsonSafe ( fp ) {
try {
return JSON . parse ( fs . readFileSync ( fp , "utf8" ) ) ;
} catch {
return { } ;
}
}
function getRelFromRoot ( filePath ) {
try {
const r = path . relative ( ROOT , filePath ) . split ( path . sep ) . join ( "/" ) ;
return r . startsWith ( "../" ) ? r : r ;
} catch {
return "" ;
}
}
function tryServeSpecialMocks ( res , relPath ) {
const rp = relPath . replace ( /^\/+/ , "" ) ;
// Serve ready JS/JSON mocks for some routes
// Digital Inputs JSON
if (
rp === "CPL/SERVICE/digitalInputs.json" ||
rp === "CPL/Service/digitalInputs.json"
) {
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"digitalInputsMockData.json"
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
// Analog Inputs JSON (case-insensitive SERVICE)
if (
rp === "CPL/SERVICE/analogInputs.json" ||
rp === "CPL/Service/analogInputs.json"
) {
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"analogInputsMockData.json"
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
// Kabelüberwachung main JS (kueData.js)
if ( rp === "CPL/SERVICE/kueData.js" ) {
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"kabelueberwachungMockData.js"
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
// Kabelüberwachung Knotenpunkte slot JS files
if ( rp . startsWith ( "CPL/SERVICE/knotenpunkte/" ) && rp . endsWith ( ".js" ) ) {
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
rp . replace ( /^CPL\// , "" )
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
// Kabelüberwachung Knotenpunkte via kueDataKnoten (case: Service)
if (
( rp . startsWith ( "CPL/Service/kueDataKnoten/" ) ||
rp . startsWith ( "CPL/SERVICE/kueDataKnoten/" ) ) &&
rp . endsWith ( ".js" )
) {
const m = rp . match ( /kueDataKnoten\/kueData(\d+)\.js$/ ) ;
if ( m ) {
const slot = Number ( m [ 1 ] ) ;
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"knotenpunkte" ,
` slot ${ slot } .js `
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
}
if ( rp === "CPL/SERVICE/systemVoltTemp.js" ) {
const base = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE"
) ;
const preferred = path . join ( base , "systemVoltTemp.js" ) ;
if ( exists ( preferred ) ) return streamRaw ( res , preferred ) ;
const mockPath = path . join ( base , "systemVoltTempMockData.js" ) ;
if ( exists ( mockPath ) ) {
// Transform variable name to expected win_systemVoltTemp when streaming
return streamTransform ( res , mockPath , ( txt ) =>
txt . replace ( /win_systemVoltTempMockData/g , "win_systemVoltTemp" )
) ;
}
}
if ( rp === "CPL/SERVICE/last20Messages.js" ) {
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"last20MessagesMockData.js"
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
// Some builds request last messages via Start.js
if ( rp === "CPL/SERVICE/Start.js" || rp === "CPL/Service/Start.js" ) {
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"last20MessagesMockData.js"
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
if ( rp === "CPL/meldungen/messages_all.json" ) {
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"meldungen" ,
"messages_all.json"
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
if ( rp . startsWith ( "CPL/chartsData/analogInputs/" ) ) {
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
rp . replace ( /^CPL\// , "" )
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
// TDR reference curves (if mocks exist)
if ( rp . startsWith ( "CPL/tdr-reference-curves/" ) ) {
const mockPath = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
rp . replace ( /^CPL\// , "" )
) ;
if ( exists ( mockPath ) ) return streamRaw ( res , mockPath ) ;
}
// Generic: any CPL/SERVICE/*.js -> try <name>MockData.js or <name>.js from mocks
const m = rp . match ( /^CPL\/(SERVICE|Service)\/([A-Za-z0-9_-]+)\.js$/ ) ;
if ( m ) {
const name = m [ 2 ] ;
const candidates = [
path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
` ${ name } MockData.js `
) ,
path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
` ${ name } .js `
) ,
] ;
for ( const p of candidates ) {
if ( exists ( p ) ) return streamRaw ( res , p ) ;
}
}
return false ;
}
function streamRaw ( res , filePath ) {
const ext = path . extname ( filePath ) . toLowerCase ( ) ;
const type = contentTypeByExt ( ext ) ;
// cache headers for static assets
const headers = { "Content-Type" : type } ;
if ( getRelFromRoot ( filePath ) . startsWith ( "_next/" ) ) {
headers [ "Cache-Control" ] = "public, max-age=31536000, immutable" ;
} else {
headers [ "Cache-Control" ] = "public, max-age=60" ;
}
res . writeHead ( 200 , headers ) ;
fs . createReadStream ( filePath ) . pipe ( res ) ;
return true ;
}
function streamTransform ( res , filePath , transformFn ) {
const ext = path . extname ( filePath ) . toLowerCase ( ) ;
const type = contentTypeByExt ( ext ) ;
const headers = { "Content-Type" : type , "Cache-Control" : "no-cache" } ;
res . writeHead ( 200 , headers ) ;
try {
const txt = fs . readFileSync ( filePath , "utf8" ) ;
const out = transformFn ( txt ) ;
res . end ( out ) ;
} catch {
res . end ( ) ;
}
return true ;
}
function resolvePlaceholders ( filePath , content ) {
const rel = getRelFromRoot ( filePath ) ;
const serviceSystem =
/CPL\/SERVICE\/(System\.(js|json)|system\.(js|json))$/i . test ( rel ) ;
const serviceOpcua = /CPL\/SERVICE\/opcua\.(js|json)$/i . test ( rel ) ;
const serviceDO = /CPL\/SERVICE\/digitalOutputs\.(js|json)$/i . test ( rel ) ;
const serviceDI = /CPL\/SERVICE\/digitalInputs\.(js|json)$/i . test ( rel ) ;
let data = { } ;
let replacer = ( m ) => m ;
if ( serviceSystem || /\.html$/i . test ( rel ) ) {
const baseDir = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE"
) ;
const sysJson = exists ( path . join ( baseDir , "SystemMockData.json" ) )
? path . join ( baseDir , "SystemMockData.json" )
: path . join ( baseDir , "systemMockData.json" ) ;
data = exists ( sysJson ) ? readJsonSafe ( sysJson ) : { } ;
const map = {
SAV00 : "win_appVersion" ,
SAN01 : "win_deviceName" ,
SEM01 : "win_mac1" ,
SEI01 : "win_ip" ,
SES01 : "win_subnet" ,
SEG01 : "win_gateway" ,
SCL01 : "win_cplInternalTimestamp" ,
STP01 : "win_ntp1" ,
STP02 : "win_ntp2" ,
STP03 : "win_ntp3" ,
STT00 : "win_ntpTimezone" ,
STA00 : "win_ntpActive" ,
SOS01 : "win_opcState" ,
SOC01 : "win_opcSessions" ,
SON01 : "win_opcName" ,
} ;
replacer = ( m , key ) => {
if ( Object . prototype . hasOwnProperty . call ( data , key ) )
return String ( data [ key ] ) ;
const prop = map [ key ] ;
if ( prop && Object . prototype . hasOwnProperty . call ( data , prop ) )
return String ( data [ prop ] ) ;
return m ;
} ;
} else if ( serviceOpcua ) {
const json = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"opcuaMockData.json"
) ;
data = exists ( json ) ? readJsonSafe ( json ) : { } ;
const map = {
SOS01 : "win_opcUaZustand" ,
SOC01 : "win_opcUaActiveClientCount" ,
SON01 : "win_opcUaNodesetName" ,
} ;
replacer = ( m , key ) => {
if ( Object . prototype . hasOwnProperty . call ( data , key ) )
return String ( data [ key ] ) ;
const prop = map [ key ] ;
if ( prop && Object . prototype . hasOwnProperty . call ( data , prop ) )
return String ( data [ prop ] ) ;
return m ;
} ;
} else if ( serviceDI ) {
const json = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"digitalInputsMockData.json"
) ;
data = exists ( json ) ? readJsonSafe ( json ) : { } ;
const getter = ( arrName , idx ) => {
const arr = data [ arrName ] ;
return Array . isArray ( arr ) && idx >= 0 && idx < arr . length
? String ( arr [ idx ] )
: undefined ;
} ;
replacer = ( m , key ) => {
// Map DES/DEN/DEC/DEF/DEG/DEI/DEZ/DEA nn (80..83) to CSV lines
const m2 = key . match ( /^(DES|DEN|DEC|DEF|DEG|DEI|DEZ|DEA)(\d{2})$/i ) ;
if ( m2 ) {
const prefix = m2 [ 1 ] . toUpperCase ( ) ;
const num = parseInt ( m2 [ 2 ] , 10 ) ;
const idx = num - 80 ; // 80..83 => 0..3
if ( idx >= 0 && idx < 4 ) {
const mapName = {
DES : "win_de_state" ,
DEN : "win_de_label" ,
DEC : "win_de_counter" ,
DEF : "win_de_time_filter" ,
DEG : "win_de_weighting" ,
DEI : "win_de_invert" ,
DEZ : "win_de_counter_active" ,
DEA : "win_de_offline" ,
} [ prefix ] ;
if ( mapName ) {
const v = getter ( mapName , idx ) ;
if ( v !== undefined ) return v ;
}
}
}
return m ;
} ;
} else if ( serviceDO ) {
const json = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"digitalOutputsMockData.json"
) ;
data = exists ( json ) ? readJsonSafe ( json ) : { } ;
replacer = ( m , key ) => {
// DASnn -> win_da_state[n-1]
if ( /^DAS\d{2}$/i . test ( key ) ) {
const idx = parseInt ( key . slice ( 3 ) , 10 ) - 1 ;
const arr = data . win _da _state ;
if ( Array . isArray ( arr ) && idx >= 0 && idx < arr . length )
return String ( arr [ idx ] ) ;
}
// DANnn -> string label (ensure proper quoting in JS array)
if ( /^DAN\d{2}$/i . test ( key ) ) {
const idx = parseInt ( key . slice ( 3 ) , 10 ) - 1 ;
const arr = data . win _da _bezeichnung ;
if ( Array . isArray ( arr ) && idx >= 0 && idx < arr . length )
return JSON . stringify ( arr [ idx ] ? ? "" ) ;
}
return m ;
} ;
} else {
// Default: no change
replacer = ( m ) => m ;
}
return content . replace ( /<%=([A-Z0-9_]+)%>/g , replacer ) ;
}
function sendFileWithReplace ( res , filePath ) {
const ext = path . extname ( filePath ) . toLowerCase ( ) ;
const type = contentTypeByExt ( ext ) ;
const rel = getRelFromRoot ( filePath ) ;
const shouldReplace =
isTextExt ( ext ) && ( ext === ".html" || /(^|\/)CPL\// . test ( rel ) ) ;
// Try cache first (only for text files)
if ( shouldReplace ) {
try {
const stat = fs . statSync ( filePath ) ;
const cached = textCache . get ( filePath ) ;
if ( cached && cached . mtimeMs === stat . mtimeMs ) {
const headers = { "Content-Type" : type , "Cache-Control" : "no-cache" } ;
res . writeHead ( 200 , headers ) ;
res . end ( cached . body ) ;
return ;
}
let content = fs . readFileSync ( filePath , "utf8" ) ;
// quick check to avoid heavy replace if no tokens
if ( content . includes ( "<%=" ) ) {
content = resolvePlaceholders ( filePath , content ) ;
}
const headers = { "Content-Type" : type , "Cache-Control" : "no-cache" } ;
res . writeHead ( 200 , headers ) ;
res . end ( content ) ;
textCache . set ( filePath , {
mtimeMs : stat . mtimeMs ,
body : content ,
contentType : type ,
} ) ;
return ;
} catch {
// fall through to stream as binary if read fails
}
}
const headers = { "Content-Type" : type } ;
if ( rel . startsWith ( "_next/" ) )
headers [ "Cache-Control" ] = "public, max-age=31536000, immutable" ;
else headers [ "Cache-Control" ] = "public, max-age=60" ;
res . writeHead ( 200 , headers ) ;
fs . createReadStream ( filePath ) . pipe ( res ) ;
}
function notFound ( res ) {
res . writeHead ( 404 , { "Content-Type" : "text/plain; charset=utf-8" } ) ;
res . end ( "Not Found" ) ;
}
const server = http . createServer ( async ( req , res ) => {
try {
const url = new URL ( req . url , ` http://localhost: ${ PORT } ` ) ;
const pathname = decodeURIComponent ( url . pathname ) ;
const rawQuery = req . url . includes ( "?" ) ? req . url . split ( "?" ) [ 1 ] : "" ;
// Lightweight API for messages used in local mocks
if ( pathname === "/api/cpl/messages" ) {
const fromDate = url . searchParams . get ( "fromDate" ) ;
const toDate = url . searchParams . get ( "toDate" ) ;
if ( ! fromDate || ! toDate ) {
res . writeHead ( 400 , { "Content-Type" : "application/json" } ) ;
res . end (
JSON . stringify ( { error : "fromDate und toDate sind erforderlich" } )
) ;
return ;
}
try {
if ( ! messagesAllCache ) {
const p = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"meldungen" ,
"messages_all.json"
) ;
const raw = fs . readFileSync ( p , "utf8" ) ;
messagesAllCache = JSON . parse ( raw ) ;
}
const from = new Date ( fromDate ) ;
const to = new Date ( toDate ) ;
to . setHours ( 23 , 59 , 59 , 999 ) ;
const filtered = messagesAllCache . filter ( ( m ) => {
const t = new Date ( m . t ) ;
return t >= from && t <= to ;
} ) ;
res . writeHead ( 200 , {
"Content-Type" : "application/json" ,
"Cache-Control" : "no-cache" ,
} ) ;
res . end ( JSON . stringify ( filtered ) ) ;
} catch {
res . writeHead ( 500 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Internal Server Error" } ) ) ;
}
return ;
}
// Dev API: digital inputs getter used by exported app on localhost
if ( pathname === "/api/cpl/getDigitalInputsHandler" ) {
try {
const p = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"digitalInputsMockData.json"
) ;
const raw = fs . readFileSync ( p , "utf8" ) ;
res . writeHead ( 200 , {
"Content-Type" : "application/json" ,
"Cache-Control" : "no-cache" ,
} ) ;
res . end ( raw ) ;
} catch {
res . writeHead ( 500 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Internal Server Error" } ) ) ;
}
return ;
}
// Dev API: analog inputs getter used by exported app on localhost
if ( pathname === "/api/cpl/getAnalogInputsHandler" ) {
try {
const p = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"analogInputsMockData.json"
) ;
const raw = fs . readFileSync ( p , "utf8" ) ;
res . writeHead ( 200 , {
"Content-Type" : "application/json" ,
"Cache-Control" : "no-cache" ,
} ) ;
res . end ( raw ) ;
} catch {
res . writeHead ( 500 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Interner Serverfehler" } ) ) ;
}
return ;
}
// Dev API: analog inputs history for charts
if ( pathname === "/api/cpl/getAnalogInputsHistory" ) {
try {
const zeitraum = url . searchParams . get ( "zeitraum" ) ; // DIA0|DIA1|DIA2
const eingangStr = url . searchParams . get ( "eingang" ) ; // 1..8
const validZ = [ "DIA0" , "DIA1" , "DIA2" ] ;
const eingang = Number ( eingangStr ) ;
if (
! validZ . includes ( zeitraum ) ||
! Number . isInteger ( eingang ) ||
eingang < 1 ||
eingang > 8
) {
res . writeHead ( 400 , { "Content-Type" : "application/json" } ) ;
res . end (
JSON . stringify ( {
error : "Ungültige Parameter" ,
erwartet : { zeitraum : "DIA0|DIA1|DIA2" , eingang : "1..8" } ,
} )
) ;
return ;
}
const fp = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"chartsData" ,
"analogInputs" ,
String ( eingang ) ,
` ${ zeitraum } .json `
) ;
if ( ! exists ( fp ) ) {
res . writeHead ( 404 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Keine Daten gefunden" } ) ) ;
return ;
}
const raw = fs . readFileSync ( fp , "utf8" ) ;
const daten = JSON . parse ( raw ) ;
res . writeHead ( 200 , {
"Content-Type" : "application/json" ,
"Cache-Control" : "no-cache" ,
} ) ;
res . end ( JSON . stringify ( { eingang , zeitraum , daten } ) ) ;
} catch {
res . writeHead ( 500 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Internal Server Error" } ) ) ;
}
return ;
}
// Dev API: update analog inputs settings (labels/offset/factor/unit/loggerInterval)
if ( pathname === "/api/cpl/updateAnalogInputsSettingsHandler" ) {
if ( req . method !== "POST" ) {
res . writeHead ( 405 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Only POST supported" } ) ) ;
return ;
}
try {
const body = await readRequestBody ( req ) ;
const updates = Array . isArray ( body ? . updates ) ? body . updates : null ;
if ( ! updates ) {
res . writeHead ( 400 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Missing updates array" } ) ) ;
return ;
}
const fp = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"analogInputsMockData.json"
) ;
const json = JSON . parse ( fs . readFileSync ( fp , "utf8" ) ) ;
for ( const u of updates ) {
const { key , index , value } = u || { } ;
if (
typeof key === "string" &&
Number . isInteger ( index ) &&
index >= 0
) {
if ( Array . isArray ( json [ key ] ) ) {
json [ key ] [ index ] = value ;
}
}
}
fs . writeFileSync ( fp , JSON . stringify ( json , null , 2 ) , "utf8" ) ;
res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { success : true } ) ) ;
} catch {
res . writeHead ( 500 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Update fehlgeschlagen" } ) ) ;
}
return ;
}
// Dev API: digital inputs updater used by UI Save button in dev/exported mode
if ( pathname === "/api/cpl/updateDigitalInputs" ) {
if ( req . method !== "POST" ) {
res . writeHead ( 405 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Only POST supported" } ) ) ;
return ;
}
try {
const body = await readRequestBody ( req ) ;
const {
id ,
label ,
invert ,
timeFilter ,
weighting ,
zaehlerAktiv ,
eingangOffline ,
} = body || { } ;
if ( typeof id !== "number" || id < 1 || id > 32 ) {
res . writeHead ( 400 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Ungültige ID (1– 32 erlaubt)" } ) ) ;
return ;
}
const fp = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"SERVICE" ,
"digitalInputsMockData.json"
) ;
const current = JSON . parse ( fs . readFileSync ( fp , "utf8" ) ) ;
const idx = id - 1 ;
if ( typeof label === "string" && Array . isArray ( current . win _de _label ) )
current . win _de _label [ idx ] = label ;
if ( typeof invert === "number" && Array . isArray ( current . win _de _invert ) )
current . win _de _invert [ idx ] = invert ;
if (
typeof timeFilter === "number" &&
Array . isArray ( current . win _de _time _filter )
)
current . win _de _time _filter [ idx ] = timeFilter ;
if (
typeof weighting === "number" &&
Array . isArray ( current . win _de _weighting )
)
current . win _de _weighting [ idx ] = weighting ;
if (
typeof zaehlerAktiv === "number" &&
Array . isArray ( current . win _de _counter _active )
)
current . win _de _counter _active [ idx ] = zaehlerAktiv ;
if (
typeof eingangOffline === "number" &&
Array . isArray ( current . win _de _offline )
)
current . win _de _offline [ idx ] = eingangOffline ;
fs . writeFileSync ( fp , JSON . stringify ( current , null , 2 ) , "utf8" ) ;
res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
res . end (
JSON . stringify ( {
message : ` Update erfolgreich für ID ${ id } ` ,
id ,
label ,
invert ,
timeFilter ,
weighting ,
eingangOffline ,
} )
) ;
} catch {
res . writeHead ( 500 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( { error : "Update fehlgeschlagen" } ) ) ;
}
return ;
}
// CPL? mapping: /CPL?/CPL/... -> /CPL/... and service commands
if ( pathname === "/CPL" && rawQuery ) {
const q = decodeURIComponent ( rawQuery ) ;
if ( q . startsWith ( "/" ) ) {
const rel = q . replace ( /^\/+/ , "" ) ;
// Special mocks
if ( tryServeSpecialMocks ( res , rel ) ) return ;
const target = path . join ( ROOT , rel ) ;
if ( exists ( target ) && fs . statSync ( target ) . isFile ( ) ) {
return sendFileWithReplace ( res , target ) ;
}
if ( exists ( target ) && fs . statSync ( target ) . isDirectory ( ) ) {
const indexFile = path . join ( target , "index.html" ) ;
if ( exists ( indexFile ) ) return sendFileWithReplace ( res , indexFile ) ;
}
return notFound ( res ) ;
}
// Service commands: implement minimal subset for messages
// Example: Service/ae.ACP&MSS1=YYYY;MM;DD;YYYY;MM;DD;All
if ( /^Service\/ae\.ACP/i . test ( q ) && q . includes ( "MSS1=" ) ) {
try {
// Extract date parameters
const m = q . match ( /MSS1=([^&]+)/ ) ;
const parts = m && m [ 1 ] ? m [ 1 ] . split ( ";" ) : [ ] ;
if ( parts . length >= 7 ) {
const [ fy , fm , fd , ty , tm , td ] = parts ;
const from = new Date ( ` ${ fy } - ${ fm } - ${ fd } ` ) ;
const to = new Date ( ` ${ ty } - ${ tm } - ${ td } ` ) ;
to . setHours ( 23 , 59 , 59 , 999 ) ;
if ( ! messagesAllCache ) {
const p = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"meldungen" ,
"messages_all.json"
) ;
const raw = fs . readFileSync ( p , "utf8" ) ;
messagesAllCache = JSON . parse ( raw ) ;
}
const filtered = messagesAllCache . filter ( ( m ) => {
const t = new Date ( m . t ) ;
return t >= from && t <= to ;
} ) ;
res . writeHead ( 200 , {
"Content-Type" : "application/json" ,
"Cache-Control" : "no-cache" ,
} ) ;
res . end ( JSON . stringify ( filtered ) ) ;
return ;
}
} catch {
// fall-through
}
}
// Service commands: history data via DIA0/DIA1/DIA2 for Analog Inputs and System Spannungen/Temperaturen
// Examples:
// - Analog: seite.ACP&DIA1=YYYY;MM;DD;YYYY;MM;DD;1xx;1 where 1xx is 100 + (eingang-1)
// - System: seite.ACP&DIA1=YYYY;MM;DD;YYYY;MM;DD;108;1 (+15V), 110 (+5V), 114 (-15V), 115 (-98V), 116 (ADC Temp), 117 (CPU Temp)
if ( /^seite\.ACP/i . test ( q ) && /DIA[0-2]=/i . test ( q ) ) {
try {
const m = q . match ( /(DIA[0-2])=([^&]+)/i ) ;
if ( m ) {
const zeitraum = m [ 1 ] . toUpperCase ( ) ;
const parts = m [ 2 ] . split ( ";" ) ;
// parts: [fy,fm,fd,ty,tm,td,channelCode(1xx), ...]
const channel = parts . length >= 7 ? Number ( parts [ 6 ] ) : NaN ;
if (
[ "DIA0" , "DIA1" , "DIA2" ] . includes ( zeitraum ) &&
Number . isFinite ( channel )
) {
// 1) Analog Inputs mapping (100..107 => Eingänge 1..8)
const eingang = channel - 99 ; // 100->1, 107->8
if ( Number . isInteger ( eingang ) && eingang >= 1 && eingang <= 8 ) {
const fp = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"chartsData" ,
"analogInputs" ,
String ( eingang ) ,
` ${ zeitraum } .json `
) ;
if ( exists ( fp ) ) {
const raw = fs . readFileSync ( fp , "utf8" ) ;
const daten = JSON . parse ( raw ) ;
res . writeHead ( 200 , {
"Content-Type" : "application/json" ,
"Cache-Control" : "no-cache" ,
} ) ;
res . end ( JSON . stringify ( daten ) ) ;
return ;
}
}
// 2) System Spannungen & Temperaturen channel mapping
const systemMap = {
108 : "systemspannung15Vplus" ,
110 : "systemspannung5Vplus" ,
114 : "systemspannung15Vminus" ,
115 : "systemspannung98Vminus" ,
116 : "temperaturADWandler" ,
117 : "temperaturProzessor" ,
} ;
const folder = systemMap [ channel ] ;
if ( folder ) {
const fp = path . join (
process . cwd ( ) ,
"mocks" ,
"device-cgi-simulator" ,
"chartsData" ,
folder ,
` ${ zeitraum } .json `
) ;
if ( exists ( fp ) ) {
const raw = fs . readFileSync ( fp , "utf8" ) ;
const daten = JSON . parse ( raw ) ;
res . writeHead ( 200 , {
"Content-Type" : "application/json" ,
"Cache-Control" : "no-cache" ,
} ) ;
res . end ( JSON . stringify ( daten ) ) ;
return ;
}
}
}
}
} catch {
// ignore malformed DIA query
}
}
// Other non-file commands: just 200 OK
res . writeHead ( 200 , { "Content-Type" : "text/plain; charset=utf-8" } ) ;
res . end ( "OK" ) ;
return ;
}
// Static serving from export
let filePath = path . join ( ROOT , pathname ) ;
if ( pathname . endsWith ( "/" ) ) {
filePath = path . join ( filePath , "index.html" ) ;
if ( exists ( filePath ) ) return sendFileWithReplace ( res , filePath ) ;
}
// Special mocks outside CPL?
const relStatic = pathname . replace ( /^\/+/ , "" ) ;
if ( tryServeSpecialMocks ( res , relStatic ) ) return ;
if ( exists ( filePath ) && fs . statSync ( filePath ) . isFile ( ) ) {
return sendFileWithReplace ( res , filePath ) ;
}
const htmlVariant = filePath + ".html" ;
if ( exists ( htmlVariant ) ) {
return sendFileWithReplace ( res , htmlVariant ) ;
}
// Fallback: out/index.html
const fallback = path . join ( ROOT , "index.html" ) ;
if ( exists ( fallback ) ) return sendFileWithReplace ( res , fallback ) ;
return notFound ( res ) ;
} catch ( err ) {
res . writeHead ( 500 , { "Content-Type" : "text/plain; charset=utf-8" } ) ;
res . end (
"Internal Server Error\n" + ( err && err . stack ? err . stack : String ( err ) )
) ;
}
} ) ;
server . listen ( PORT , ( ) => {
console . log ( ` Local CPL simulator running on http://localhost: ${ PORT } ` ) ;
console . log ( ` Serving from: ${ ROOT } ` ) ;
console . log ( "Replacing CGI placeholders using mocks/ where available" ) ;
} ) ;