This commit is contained in:
Marcelo
2025-12-18 20:17:20 +00:00
parent 0e9b2dd72d
commit ffc39a5c90
9 changed files with 1268 additions and 69 deletions

View File

@@ -0,0 +1,46 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
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 });
const body = await req.json().catch(() => null);
if (!body?.machineId || !body?.cycle) {
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 });
const c = body.cycle;
const tsMs =
(typeof c.timestamp === "number" && c.timestamp) ||
(typeof c.ts === "number" && c.ts) ||
(typeof c.event_timestamp === "number" && c.event_timestamp) ||
undefined;
const ts = tsMs ? new Date(tsMs) : new Date();
const row = await prisma.machineCycle.create({
data: {
orgId: machine.orgId,
machineId: machine.id,
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,
workOrderId: c.work_order_id ? String(c.work_order_id) : null,
sku: c.sku ? String(c.sku) : null,
cavities: typeof c.cavities === "number" ? c.cavities : null,
goodDelta: typeof c.good_delta === "number" ? c.good_delta : null,
scrapDelta: typeof c.scrap_delta === "number" ? c.scrap_delta : null,
},
});
return NextResponse.json({ ok: true, id: row.id, ts: row.ts });
}

View File

@@ -22,18 +22,25 @@ export async function POST(req: Request) {
const e = body.event;
const rawEvent = body.event;
const e = Array.isArray(rawEvent) ? rawEvent[0] : rawEvent;
const ts =
typeof e?.data?.timestamp === "number"
? new Date(e.data.timestamp)
: undefined;
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 ?? "";
// normalize inputs from event
const sev = String(e.severity ?? "").toLowerCase();
const typ = String(e.eventType ?? e.anomaly_type ?? "").toLowerCase();
const title = String(e.title ?? "").trim();
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",
@@ -43,35 +50,58 @@ const ALLOWED_TYPES = new Set([
"microstop",
]);
const ALLOWED_SEVERITIES = new Set(["warning", "critical"]);
// Drop generic/noise
if (!ALLOWED_SEVERITIES.has(sev) || !ALLOWED_TYPES.has(typ)) {
return NextResponse.json({ ok: true, skipped: true }, { status: 200 });
if (!ALLOWED_TYPES.has(typ)) {
return NextResponse.json({ ok: true, skipped: true, reason: "type_not_allowed", typ, sev }, { status: 200 });
}
if (!title) return NextResponse.json({ ok: true, skipped: true }, { 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 });
const row = await prisma.machineEvent.create({
data: {
orgId: machine.orgId,
machineId: machine.id,
ts: ts ?? undefined,
topic: e.topic ? String(e.topic) : "event",
eventType: e.anomaly_type ? String(e.anomaly_type) : "unknown",
severity: e.severity ? String(e.severity) : "info",
requiresAck: !!e.requires_ack,
title: e.title ? String(e.title) : "Event",
description: e.description ? String(e.description) : null,
data: e.data ?? e, // store full blob
workOrderId: e?.data?.work_order_id ? String(e.data.work_order_id) : null,
},
});
return NextResponse.json({ ok: true, id: row.id, ts: row.ts });
}

View File

@@ -3,17 +3,102 @@ import type { NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireSession } from "@/lib/auth/requireSession";
function normalizeEvent(row: any) {
// data can be object OR [object]
const raw = row.data;
const blob = Array.isArray(raw) ? raw[0] : raw;
// some payloads nest details under blob.data
const inner = blob?.data ?? blob ?? {};
const normalizeType = (t: any) =>
String(t ?? "")
.trim()
.toLowerCase()
.replace(/_/g, "-");
// Prefer the DB columns if they are meaningful
const fromDbType = row.eventType && row.eventType !== "unknown" ? row.eventType : null;
const fromBlobType = blob?.anomaly_type ?? blob?.eventType ?? blob?.topic ?? inner?.anomaly_type ?? inner?.eventType ?? null;
// infer slow-cycle if the signature exists
const inferredType =
fromDbType ??
fromBlobType ??
((inner?.actual_cycle_time && inner?.theoretical_cycle_time) || (blob?.actual_cycle_time && blob?.theoretical_cycle_time)
? "slow-cycle"
: "unknown");
const eventType = normalizeType(inferredType);
const severity =
String(
(row.severity && row.severity !== "info" ? row.severity : null) ??
blob?.severity ??
inner?.severity ??
"info"
)
.trim()
.toLowerCase();
const title =
String(
(row.title && row.title !== "Event" ? row.title : null) ??
blob?.title ??
inner?.title ??
(eventType === "slow-cycle" ? "Slow Cycle Detected" : "Event")
).trim();
const description =
row.description ??
blob?.description ??
inner?.description ??
(eventType === "slow-cycle" &&
inner?.actual_cycle_time &&
inner?.theoretical_cycle_time &&
inner?.delta_percent != null
? `Cycle took ${Number(inner.actual_cycle_time).toFixed(1)}s (+${inner.delta_percent}% vs ${Number(inner.theoretical_cycle_time).toFixed(1)}s objetivo)`
: null);
const ts =
row.ts ??
(typeof blob?.timestamp === "number" ? new Date(blob.timestamp) : null) ??
(typeof inner?.timestamp === "number" ? new Date(inner.timestamp) : null) ??
null;
const workOrderId =
row.workOrderId ??
blob?.work_order_id ??
inner?.work_order_id ??
null;
return {
id: row.id,
ts,
topic: String(row.topic ?? blob?.topic ?? eventType),
eventType,
severity,
title,
description,
requiresAck: !!row.requiresAck,
workOrderId,
};
}
export async function GET(
_req: NextRequest,
{ params }: { params: { machineId: string } }
{ params }: { params: Promise<{ machineId: string }> }
) {
const session = await requireSession();
if (!session) {
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
}
const { machineId } = params;
const { machineId } = await params;
const machine = await prisma.machine.findFirst({
where: { id: machineId, orgId: session.orgId },
@@ -51,18 +136,85 @@ export async function GET(
return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
}
const events = await prisma.machineEvent.findMany({
where: {
orgId: session.orgId,
machineId,
severity: { in: ["warning", "critical"] },
eventType: { in: ["slow-cycle", "anomaly-detected", "performance-degradation", "scrap-spike", "down", "microstop"] },
},
orderBy: { ts: "desc" },
take: 30,
select: { /* same as now */ },
const rawEvents = await prisma.machineEvent.findMany({
where: {
orgId: session.orgId,
machineId,
},
orderBy: { ts: "desc" },
take: 100, // pull more, we'll filter after normalization
select: {
id: true,
ts: true,
topic: true,
eventType: true,
severity: true,
title: true,
description: true,
requiresAck: true,
data: true,
workOrderId: true,
},
});
const normalized = rawEvents.map(normalizeEvent);
const ALLOWED_TYPES = new Set([
"slow-cycle",
"anomaly-detected",
"performance-degradation",
"scrap-spike",
"down",
"microstop",
]);
const events = normalized
.filter((e) => ALLOWED_TYPES.has(e.eventType))
// keep slow-cycle even if severity is info, otherwise require warning/critical/error
.filter((e) => e.eventType === "slow-cycle" || ["warning", "critical", "error"].includes(e.severity))
.slice(0, 30);
const rawCycles = await prisma.machineCycle.findMany({
where: { orgId: session.orgId, machineId },
orderBy: { ts: "desc" },
take: 200,
select: {
ts: true,
cycleCount: true,
actualCycleTime: true,
theoreticalCycleTime: true,
workOrderId: true,
sku: true,
},
});
// chart-friendly: oldest -> newest + numeric timestamps
const cycles = rawCycles
.slice()
.reverse()
.map((c) => ({
ts: c.ts, // keep Date for “time ago” UI
t: c.ts.getTime(), // numeric x-axis for charts
cycleCount: c.cycleCount ?? null,
actual: c.actualCycleTime, // rename to what chart expects
ideal: c.theoreticalCycleTime ?? null,
workOrderId: c.workOrderId ?? null,
sku: c.sku ?? null,
}
));
const latestKpi = machine.kpiSnapshots[0] ?? null;
// rawCycles is ordered DESC, so [0] is the most recent cycle row
const latestCycleIdeal = rawCycles[0]?.theoreticalCycleTime ?? null;
// REAL effective value (not mock): prefer KPI if present, else fallback to cycles table
const effectiveCycleTime = latestKpi?.cycleTime ?? latestCycleIdeal ?? null;
return NextResponse.json({
ok: true,
machine: {
@@ -72,7 +224,14 @@ export async function GET(
location: machine.location,
latestHeartbeat: machine.heartbeats[0] ?? null,
latestKpi: machine.kpiSnapshots[0] ?? null,
effectiveCycleTime
},
events,
cycles
});
}