Files
CPLv4.0/scripts/local-cpl-sim.mjs

927 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 (132 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");
});