Issues with data flow & consistency

This commit is contained in:
Marcelo
2025-12-22 14:36:40 +00:00
parent ffc39a5c90
commit 945ff2dc09
4 changed files with 414 additions and 121 deletions

View File

@@ -30,7 +30,7 @@ export async function POST(req: Request) {
data: {
orgId: machine.orgId,
machineId: machine.id,
ts,
ts,
cycleCount: typeof c.cycle_count === "number" ? c.cycle_count : null,
actualCycleTime: Number(c.actual_cycle_time),
theoreticalCycleTime: c.theoretical_cycle_time != null ? Number(c.theoretical_cycle_time) : null,
@@ -41,6 +41,5 @@ export async function POST(req: Request) {
scrapDelta: typeof c.scrap_delta === "number" ? c.scrap_delta : null,
},
});
return NextResponse.json({ ok: true, id: row.id, ts: row.ts });
}

View File

@@ -1,107 +1,153 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
const normalizeType = (t: any) =>
String(t ?? "")
.trim()
.toLowerCase()
.replace(/_/g, "-");
const CANON_TYPE: Record<string, string> = {
// Node-RED
"production-stopped": "stop",
"oee-drop": "oee-drop",
"quality-spike": "quality-spike",
"predictive-oee-decline": "predictive-oee-decline",
"performance-degradation": "performance-degradation",
// legacy / synonyms
"macroparo": "macrostop",
"macro-stop": "macrostop",
"microparo": "microstop",
"micro-paro": "microstop",
"down": "stop",
};
const ALLOWED_TYPES = new Set([
"slow-cycle",
"microstop",
"macrostop",
"oee-drop",
"quality-spike",
"performance-degradation",
"predictive-oee-decline",
]);
// thresholds for stop classification (tune later / move to machine config)
const MICROSTOP_SEC = 60;
const MACROSTOP_SEC = 300;
export async function POST(req: Request) {
const apiKey = req.headers.get("x-api-key");
if (!apiKey) return NextResponse.json({ ok: false, error: "Missing api key" }, { status: 401 });
if (!apiKey) {
return NextResponse.json({ ok: false, error: "Missing api key" }, { status: 401 });
}
const body = await req.json().catch(() => null);
if (!body?.machineId || !body?.event) {
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
}
const machine = await prisma.machine.findFirst({
where: { id: String(body.machineId), apiKey },
select: { id: true, orgId: true },
});
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
if (!machine) {
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
}
// Normalize to array (Node-RED sends array of anomalies)
const rawEvent = body.event;
const events = Array.isArray(rawEvent) ? rawEvent : [rawEvent];
// Convert ms epoch -> Date if provided
const created: { id: string; ts: Date; eventType: string }[] = [];
const skipped: any[] = [];
const rawEvent = body.event;
const e = Array.isArray(rawEvent) ? rawEvent[0] : rawEvent;
if (!e || typeof e !== "object") {
return NextResponse.json({ ok: false, error: "Invalid event object" }, { status: 400 });
}
const rawType =
e.eventType ?? e.anomaly_type ?? e.topic ?? body.topic ?? "";
const normalizeType = (t: string) =>
String(t)
.trim()
.toLowerCase()
.replace(/_/g, "-");
const typ = normalizeType(rawType);
const sev = String(e.severity ?? "").trim().toLowerCase();
// accept these types
const ALLOWED_TYPES = new Set([
"slow-cycle",
"anomaly-detected",
"performance-degradation",
"scrap-spike",
"down",
"microstop",
]);
if (!ALLOWED_TYPES.has(typ)) {
return NextResponse.json({ ok: true, skipped: true, reason: "type_not_allowed", typ, sev }, { status: 200 });
}
// optional: severity enforcement only for SOME types (not slow-cycle)
const NEEDS_HIGH_SEV = new Set(["down", "scrap-spike"]);
const ALLOWED_SEVERITIES = new Set(["warning", "critical", "error"]);
if (NEEDS_HIGH_SEV.has(typ) && !ALLOWED_SEVERITIES.has(sev)) {
return NextResponse.json({ ok: true, skipped: true, reason: "severity_too_low", typ, sev }, { status: 200 });
}
// timestamp handling (support multiple field names)
const tsMs =
(typeof (e as any)?.timestamp === "number" && (e as any).timestamp) ||
(typeof e?.data?.timestamp === "number" && e.data.timestamp) ||
(typeof e?.data?.event_timestamp === "number" && e.data.event_timestamp) ||
(typeof e?.data?.ts === "number" && e.data.ts) ||
undefined;
const ts = tsMs ? new Date(tsMs) : new Date(); // default to now if missing
const title =
String(e.title ?? "").trim() ||
(typ === "slow-cycle" ? "Slow Cycle Detected" : "Event");
const description = e.description
? String(e.description)
: null;
const row = await prisma.machineEvent.create({
data: {
orgId: machine.orgId,
machineId: machine.id,
ts,
topic: String(e.topic ?? typ),
eventType: typ, // ✅ store normalized type
severity: sev || "info", // ✅ store normalized severity
requiresAck: !!e.requires_ack,
title,
description,
data: e.data ?? e,
workOrderId:
(e as any)?.work_order_id ? String((e as any).work_order_id)
: e?.data?.work_order_id ? String(e.data.work_order_id)
: null,
},
});
return NextResponse.json({ ok: true, id: row.id, ts: row.ts });
for (const ev of events) {
if (!ev || typeof ev !== "object") {
skipped.push({ reason: "invalid_event_object" });
continue;
}
const rawType = (ev as any).eventType ?? (ev as any).anomaly_type ?? (ev as any).topic ?? body.topic ?? "";
const typ0 = normalizeType(rawType);
const typ = CANON_TYPE[typ0] ?? typ0;
// Determine timestamp
const tsMs =
(typeof (ev as any)?.timestamp === "number" && (ev as any).timestamp) ||
(typeof (ev as any)?.data?.timestamp === "number" && (ev as any).data.timestamp) ||
(typeof (ev as any)?.data?.event_timestamp === "number" && (ev as any).data.event_timestamp) ||
null;
const ts = tsMs ? new Date(tsMs) : new Date();
// Severity defaulting (do not skip on severity — store for audit)
let sev = String((ev as any).severity ?? "").trim().toLowerCase();
if (!sev) sev = "warning";
// Stop classification -> microstop/macrostop
let finalType = typ;
if (typ === "stop") {
const stopSec =
(typeof (ev as any)?.data?.stoppage_duration_seconds === "number" && (ev as any).data.stoppage_duration_seconds) ||
(typeof (ev as any)?.data?.stop_duration_seconds === "number" && (ev as any).data.stop_duration_seconds) ||
null;
if (stopSec != null) {
finalType = stopSec >= MACROSTOP_SEC ? "macrostop" : "microstop";
} else {
// missing duration -> conservative
finalType = "microstop";
}
}
if (!ALLOWED_TYPES.has(finalType)) {
skipped.push({ reason: "type_not_allowed", typ: finalType, sev });
continue;
}
const title =
String((ev as any).title ?? "").trim() ||
(finalType === "slow-cycle" ? "Slow Cycle Detected" :
finalType === "macrostop" ? "Macrostop Detected" :
finalType === "microstop" ? "Microstop Detected" :
"Event");
const description = (ev as any).description ? String((ev as any).description) : null;
// store full blob, ensure object
const rawData = (ev as any).data ?? ev;
const dataObj = typeof rawData === "string" ? (() => {
try { return JSON.parse(rawData); } catch { return { raw: rawData }; }
})() : rawData;
const row = await prisma.machineEvent.create({
data: {
orgId: machine.orgId,
machineId: machine.id,
ts,
topic: String((ev as any).topic ?? finalType),
eventType: finalType,
severity: sev,
requiresAck: !!(ev as any).requires_ack,
title,
description,
data: dataObj,
workOrderId:
(ev as any)?.work_order_id ? String((ev as any).work_order_id)
: (ev as any)?.data?.work_order_id ? String((ev as any).data.work_order_id)
: null,
sku:
(ev as any)?.sku ? String((ev as any).sku)
: (ev as any)?.data?.sku ? String((ev as any).data.sku)
: null,
},
});
created.push({ id: row.id, ts: row.ts, eventType: row.eventType });
}
return NextResponse.json({ ok: true, createdCount: created.length, created, skippedCount: skipped.length, skipped });
}