From b62c477d50afdbfb9e0a7dec8ea3529c7054fb69 Mon Sep 17 00:00:00 2001 From: ISA Date: Thu, 4 Sep 2025 11:02:17 +0200 Subject: [PATCH] feat: local-cpl-sim.mjs Einstellungen done --- .env.development | 2 +- .env.production | 2 +- CHANGELOG.md | 5 + package-lock.json | 4 +- package.json | 2 +- scripts/local-cpl-sim.mjs | 203 +++++++++++++++++++++++++++++++++++--- 6 files changed, 200 insertions(+), 18 deletions(-) diff --git a/.env.development b/.env.development index a304b44..2d058cc 100644 --- a/.env.development +++ b/.env.development @@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false NEXT_PUBLIC_EXPORT_STATIC=false NEXT_PUBLIC_USE_CGI=false # App-Versionsnummer -NEXT_PUBLIC_APP_VERSION=1.6.833 +NEXT_PUBLIC_APP_VERSION=1.6.834 NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter) diff --git a/.env.production b/.env.production index 93ceb9c..723da56 100644 --- a/.env.production +++ b/.env.production @@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL NEXT_PUBLIC_EXPORT_STATIC=true NEXT_PUBLIC_USE_CGI=true # App-Versionsnummer -NEXT_PUBLIC_APP_VERSION=1.6.833 +NEXT_PUBLIC_APP_VERSION=1.6.834 NEXT_PUBLIC_CPL_MODE=production \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c596ac6..4dee412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [1.6.834] – 2025-09-04 + +- feat: local-cpl-sim.mjs + +--- ## [1.6.833] – 2025-09-04 - test: npx playwright test erfolgreich diff --git a/package-lock.json b/package-lock.json index 7fece6e..e952500 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cpl-v4", - "version": "1.6.833", + "version": "1.6.834", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cpl-v4", - "version": "1.6.833", + "version": "1.6.834", "dependencies": { "@fontsource/roboto": "^5.1.0", "@headlessui/react": "^2.2.4", diff --git a/package.json b/package.json index 6684f22..e5115b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cpl-v4", - "version": "1.6.833", + "version": "1.6.834", "private": true, "scripts": { "dev": "next dev -p 3000", diff --git a/scripts/local-cpl-sim.mjs b/scripts/local-cpl-sim.mjs index f142bdc..7773e03 100644 --- a/scripts/local-cpl-sim.mjs +++ b/scripts/local-cpl-sim.mjs @@ -1,6 +1,6 @@ -// Minimal local simulator for testing SAN01 replacement only. +// Local simulator that serves the exported build and replaces CGI placeholders from mocks/. // - Serves the exported "out" folder. -// - For text files (.html, .js, .json, .css, .txt) replaces <%=SAN01%> -> "ismail" on the fly. +// - 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"; @@ -49,13 +49,185 @@ function isTextExt(ext) { return [".html", ".js", ".json", ".css", ".txt", ".svg"].includes(ext); } -function sendFileWithSAN01Replace(res, filePath) { +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 + if (rp === "CPL/SERVICE/systemVoltTemp.js") { + const mockPath = path.join( + process.cwd(), + "mocks", + "device-cgi-simulator", + "SERVICE", + "systemVoltTempMockData.js" + ); + if (exists(mockPath)) return streamRaw(res, mockPath); + } + 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); + } + 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); + } + return false; +} + +function streamRaw(res, filePath) { + const ext = path.extname(filePath).toLowerCase(); + const type = contentTypeByExt(ext); + res.writeHead(200, { "Content-Type": type }); + fs.createReadStream(filePath).pipe(res); + 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); + + 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 (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); if (isTextExt(ext)) { try { let content = fs.readFileSync(filePath, "utf8"); - content = content.replace(/<%=SAN01%>/g, "ismail"); + content = resolvePlaceholders(filePath, content); res.writeHead(200, { "Content-Type": type }); res.end(content); return; @@ -78,19 +250,20 @@ const server = http.createServer((req, res) => { const pathname = decodeURIComponent(url.pathname); const rawQuery = req.url.includes("?") ? req.url.split("?")[1] : ""; - // Minimal CPL? mapping: /CPL?/CPL/... -> /CPL/... + // CPL? mapping: /CPL?/CPL/... -> /CPL/... 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 sendFileWithSAN01Replace(res, target); + return sendFileWithReplace(res, target); } if (exists(target) && fs.statSync(target).isDirectory()) { const indexFile = path.join(target, "index.html"); - if (exists(indexFile)) - return sendFileWithSAN01Replace(res, indexFile); + if (exists(indexFile)) return sendFileWithReplace(res, indexFile); } return notFound(res); } @@ -105,21 +278,25 @@ const server = http.createServer((req, res) => { if (pathname.endsWith("/")) { filePath = path.join(filePath, "index.html"); - if (exists(filePath)) return sendFileWithSAN01Replace(res, filePath); + 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 sendFileWithSAN01Replace(res, filePath); + return sendFileWithReplace(res, filePath); } const htmlVariant = filePath + ".html"; if (exists(htmlVariant)) { - return sendFileWithSAN01Replace(res, htmlVariant); + return sendFileWithReplace(res, htmlVariant); } // Fallback: out/index.html const fallback = path.join(ROOT, "index.html"); - if (exists(fallback)) return sendFileWithSAN01Replace(res, fallback); + if (exists(fallback)) return sendFileWithReplace(res, fallback); return notFound(res); } catch (err) { @@ -133,5 +310,5 @@ const server = http.createServer((req, res) => { server.listen(PORT, () => { console.log(`Local CPL simulator running on http://localhost:${PORT}`); console.log(`Serving from: ${ROOT}`); - console.log("Replacing <%=SAN01%> -> ismail in text responses"); + console.log("Replacing CGI placeholders using mocks/ where available"); });