|
|
|
|
@@ -10,6 +10,10 @@ 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
|
|
|
|
|
|
|
|
|
|
function exists(p) {
|
|
|
|
|
try {
|
|
|
|
|
fs.accessSync(p, fs.constants.F_OK);
|
|
|
|
|
@@ -80,6 +84,17 @@ function tryServeSpecialMocks(res, relPath) {
|
|
|
|
|
);
|
|
|
|
|
if (exists(mockPath)) return streamRaw(res, mockPath);
|
|
|
|
|
}
|
|
|
|
|
// Analog Inputs JSON
|
|
|
|
|
if (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(
|
|
|
|
|
@@ -101,6 +116,26 @@ function tryServeSpecialMocks(res, relPath) {
|
|
|
|
|
);
|
|
|
|
|
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 mockPath = path.join(
|
|
|
|
|
process.cwd(),
|
|
|
|
|
@@ -121,6 +156,17 @@ function tryServeSpecialMocks(res, relPath) {
|
|
|
|
|
);
|
|
|
|
|
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(),
|
|
|
|
|
@@ -150,13 +196,44 @@ function tryServeSpecialMocks(res, relPath) {
|
|
|
|
|
);
|
|
|
|
|
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);
|
|
|
|
|
res.writeHead(200, { "Content-Type": type });
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
@@ -167,6 +244,7 @@ function resolvePlaceholders(filePath, content) {
|
|
|
|
|
/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;
|
|
|
|
|
@@ -229,6 +307,47 @@ function resolvePlaceholders(filePath, content) {
|
|
|
|
|
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(),
|
|
|
|
|
@@ -266,18 +385,44 @@ function resolvePlaceholders(filePath, content) {
|
|
|
|
|
function sendFileWithReplace(res, filePath) {
|
|
|
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
|
|
|
const type = contentTypeByExt(ext);
|
|
|
|
|
if (isTextExt(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);
|
|
|
|
|
res.writeHead(200, { "Content-Type": type });
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res.writeHead(200, { "Content-Type": type });
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -292,7 +437,49 @@ const server = http.createServer((req, res) => {
|
|
|
|
|
const pathname = decodeURIComponent(url.pathname);
|
|
|
|
|
const rawQuery = req.url.includes("?") ? req.url.split("?")[1] : "";
|
|
|
|
|
|
|
|
|
|
// CPL? mapping: /CPL?/CPL/... -> /CPL/...
|
|
|
|
|
// 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: "Interner Serverfehler" }));
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CPL? mapping: /CPL?/CPL/... -> /CPL/... and service commands
|
|
|
|
|
if (pathname === "/CPL" && rawQuery) {
|
|
|
|
|
const q = decodeURIComponent(rawQuery);
|
|
|
|
|
if (q.startsWith("/")) {
|
|
|
|
|
@@ -309,7 +496,45 @@ const server = http.createServer((req, res) => {
|
|
|
|
|
}
|
|
|
|
|
return notFound(res);
|
|
|
|
|
}
|
|
|
|
|
// Non-file commands: just 200 OK
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Other non-file commands: just 200 OK
|
|
|
|
|
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
|
|
|
res.end("OK");
|
|
|
|
|
return;
|
|
|
|
|
|