Compare commits
8 Commits
57ffdecb10
...
6820fa9eed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6820fa9eed | ||
|
|
3daa6b1dbb | ||
|
|
9c7ad37233 | ||
|
|
0286670b81 | ||
|
|
02a0ce5891 | ||
|
|
47e0efeb80 | ||
|
|
b62c477d50 | ||
|
|
653a31ce63 |
@@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false
|
|||||||
NEXT_PUBLIC_EXPORT_STATIC=false
|
NEXT_PUBLIC_EXPORT_STATIC=false
|
||||||
NEXT_PUBLIC_USE_CGI=false
|
NEXT_PUBLIC_USE_CGI=false
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.6.832
|
NEXT_PUBLIC_APP_VERSION=1.6.840
|
||||||
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)
|
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL
|
|||||||
NEXT_PUBLIC_EXPORT_STATIC=true
|
NEXT_PUBLIC_EXPORT_STATIC=true
|
||||||
NEXT_PUBLIC_USE_CGI=true
|
NEXT_PUBLIC_USE_CGI=true
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.6.832
|
NEXT_PUBLIC_APP_VERSION=1.6.840
|
||||||
NEXT_PUBLIC_CPL_MODE=production
|
NEXT_PUBLIC_CPL_MODE=production
|
||||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,3 +1,43 @@
|
|||||||
|
## [1.6.840] – 2025-09-04
|
||||||
|
|
||||||
|
- feat: local-cpl-sim.mjs analogInputs /Messwerteingäge / analoge Eingänge
|
||||||
|
|
||||||
|
---
|
||||||
|
## [1.6.839] – 2025-09-04
|
||||||
|
|
||||||
|
- feat: local-cpl-sim.mjs digitalInputs /Messwerteingänge
|
||||||
|
|
||||||
|
---
|
||||||
|
## [1.6.838] – 2025-09-04
|
||||||
|
|
||||||
|
- feat: local-cpl-sim system
|
||||||
|
|
||||||
|
---
|
||||||
|
## [1.6.837] – 2025-09-04
|
||||||
|
|
||||||
|
- feat: local-cpl-sim meldungen/Berichte
|
||||||
|
|
||||||
|
---
|
||||||
|
## [1.6.836] – 2025-09-04
|
||||||
|
|
||||||
|
- feat: local-cpl-sim.mjs kabelueberwachung
|
||||||
|
|
||||||
|
---
|
||||||
|
## [1.6.835] – 2025-09-04
|
||||||
|
|
||||||
|
- feat: local-cpl-sim.mjs Einstellungen done
|
||||||
|
|
||||||
|
---
|
||||||
|
## [1.6.834] – 2025-09-04
|
||||||
|
|
||||||
|
- feat: local-cpl-sim.mjs
|
||||||
|
|
||||||
|
---
|
||||||
|
## [1.6.833] – 2025-09-04
|
||||||
|
|
||||||
|
- test: npx playwright test erfolgreich
|
||||||
|
|
||||||
|
---
|
||||||
## [1.6.832] – 2025-09-03
|
## [1.6.832] – 2025-09-03
|
||||||
|
|
||||||
- refactoring: test files
|
- refactoring: test files
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "cpl-v4",
|
"name": "cpl-v4",
|
||||||
"version": "1.6.832",
|
"version": "1.6.840",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cpl-v4",
|
"name": "cpl-v4",
|
||||||
"version": "1.6.832",
|
"version": "1.6.840",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/roboto": "^5.1.0",
|
"@fontsource/roboto": "^5.1.0",
|
||||||
"@headlessui/react": "^2.2.4",
|
"@headlessui/react": "^2.2.4",
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "cpl-v4",
|
"name": "cpl-v4",
|
||||||
"version": "1.6.832",
|
"version": "1.6.840",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000",
|
"dev": "next dev -p 3000",
|
||||||
"clean": "rimraf .next out",
|
"clean": "rimraf .next out",
|
||||||
"build": "npm run clean && cross-env EXPORT_STATIC=true next build",
|
"build": "npm run clean && cross-env EXPORT_STATIC=true next build",
|
||||||
"postbuild": "copy LICENSE_ICONIFY.txt out\\LICENSE_ICONIFY.txt",
|
"postbuild": "copy LICENSE_ICONIFY.txt out\\LICENSE_ICONIFY.txt",
|
||||||
|
"serve:sim": "node ./scripts/local-cpl-sim.mjs",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"check": "npm run lint && npm run typecheck",
|
"check": "npm run lint && npm run typecheck",
|
||||||
|
|||||||
926
scripts/local-cpl-sim.mjs
Normal file
926
scripts/local-cpl-sim.mjs
Normal file
@@ -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");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user