feat: local-cpl-sim meldungen/Berichte

This commit is contained in:
ISA
2025-09-04 12:02:04 +02:00
parent 47e0efeb80
commit 02a0ce5891
6 changed files with 242 additions and 12 deletions

View File

@@ -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.835 NEXT_PUBLIC_APP_VERSION=1.6.836
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)

View File

@@ -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.835 NEXT_PUBLIC_APP_VERSION=1.6.836
NEXT_PUBLIC_CPL_MODE=production NEXT_PUBLIC_CPL_MODE=production

View File

@@ -1,3 +1,8 @@
## [1.6.836] 2025-09-04
- feat: local-cpl-sim.mjs kabelueberwachung
---
## [1.6.835] 2025-09-04 ## [1.6.835] 2025-09-04
- feat: local-cpl-sim.mjs Einstellungen done - feat: local-cpl-sim.mjs Einstellungen done

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.835", "version": "1.6.836",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.835", "version": "1.6.836",
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.1.0", "@fontsource/roboto": "^5.1.0",
"@headlessui/react": "^2.2.4", "@headlessui/react": "^2.2.4",

View File

@@ -1,6 +1,6 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.835", "version": "1.6.836",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000",

View File

@@ -10,6 +10,10 @@ import path from "path";
const PORT = process.env.PORT ? Number(process.env.PORT) : 3030; const PORT = process.env.PORT ? Number(process.env.PORT) : 3030;
const ROOT = path.join(process.cwd(), "out"); 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) { function exists(p) {
try { try {
fs.accessSync(p, fs.constants.F_OK); fs.accessSync(p, fs.constants.F_OK);
@@ -80,6 +84,17 @@ function tryServeSpecialMocks(res, relPath) {
); );
if (exists(mockPath)) return streamRaw(res, mockPath); 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) // Kabelüberwachung main JS (kueData.js)
if (rp === "CPL/SERVICE/kueData.js") { if (rp === "CPL/SERVICE/kueData.js") {
const mockPath = path.join( const mockPath = path.join(
@@ -101,6 +116,26 @@ function tryServeSpecialMocks(res, relPath) {
); );
if (exists(mockPath)) return streamRaw(res, mockPath); 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") { if (rp === "CPL/SERVICE/systemVoltTemp.js") {
const mockPath = path.join( const mockPath = path.join(
process.cwd(), process.cwd(),
@@ -121,6 +156,17 @@ function tryServeSpecialMocks(res, relPath) {
); );
if (exists(mockPath)) return streamRaw(res, mockPath); 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") { if (rp === "CPL/meldungen/messages_all.json") {
const mockPath = path.join( const mockPath = path.join(
process.cwd(), process.cwd(),
@@ -150,13 +196,44 @@ function tryServeSpecialMocks(res, relPath) {
); );
if (exists(mockPath)) return streamRaw(res, mockPath); 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; return false;
} }
function streamRaw(res, filePath) { function streamRaw(res, filePath) {
const ext = path.extname(filePath).toLowerCase(); const ext = path.extname(filePath).toLowerCase();
const type = contentTypeByExt(ext); 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); fs.createReadStream(filePath).pipe(res);
return true; return true;
} }
@@ -167,6 +244,7 @@ function resolvePlaceholders(filePath, content) {
/CPL\/SERVICE\/(System\.(js|json)|system\.(js|json))$/i.test(rel); /CPL\/SERVICE\/(System\.(js|json)|system\.(js|json))$/i.test(rel);
const serviceOpcua = /CPL\/SERVICE\/opcua\.(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 serviceDO = /CPL\/SERVICE\/digitalOutputs\.(js|json)$/i.test(rel);
const serviceDI = /CPL\/SERVICE\/digitalInputs\.(js|json)$/i.test(rel);
let data = {}; let data = {};
let replacer = (m) => m; let replacer = (m) => m;
@@ -229,6 +307,47 @@ function resolvePlaceholders(filePath, content) {
return String(data[prop]); return String(data[prop]);
return m; 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) { } else if (serviceDO) {
const json = path.join( const json = path.join(
process.cwd(), process.cwd(),
@@ -266,18 +385,44 @@ function resolvePlaceholders(filePath, content) {
function sendFileWithReplace(res, filePath) { function sendFileWithReplace(res, filePath) {
const ext = path.extname(filePath).toLowerCase(); const ext = path.extname(filePath).toLowerCase();
const type = contentTypeByExt(ext); 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 { 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"); let content = fs.readFileSync(filePath, "utf8");
content = resolvePlaceholders(filePath, content); // quick check to avoid heavy replace if no tokens
res.writeHead(200, { "Content-Type": type }); if (content.includes("<%=")) {
content = resolvePlaceholders(filePath, content);
}
const headers = { "Content-Type": type, "Cache-Control": "no-cache" };
res.writeHead(200, headers);
res.end(content); res.end(content);
textCache.set(filePath, {
mtimeMs: stat.mtimeMs,
body: content,
contentType: type,
});
return; return;
} catch { } catch {
// fall through to stream as binary if read fails // 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); fs.createReadStream(filePath).pipe(res);
} }
@@ -292,7 +437,49 @@ const server = http.createServer((req, res) => {
const pathname = decodeURIComponent(url.pathname); const pathname = decodeURIComponent(url.pathname);
const rawQuery = req.url.includes("?") ? req.url.split("?")[1] : ""; 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) { if (pathname === "/CPL" && rawQuery) {
const q = decodeURIComponent(rawQuery); const q = decodeURIComponent(rawQuery);
if (q.startsWith("/")) { if (q.startsWith("/")) {
@@ -309,7 +496,45 @@ const server = http.createServer((req, res) => {
} }
return notFound(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.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("OK"); res.end("OK");
return; return;