Mobile friendly, lint correction, typescript error clear

This commit is contained in:
Marcelo
2026-01-16 22:39:16 +00:00
parent 0f88207f3f
commit c183dda383
58 changed files with 7199 additions and 2714 deletions

View File

@@ -3,27 +3,33 @@ import { prisma } from "@/lib/prisma";
import { getMachineAuth } from "@/lib/machineAuthCache";
import { z } from "zod";
function unwrapEnvelope(raw: any) {
if (!raw || typeof raw !== "object") return raw;
const payload = raw.payload;
if (!payload || typeof payload !== "object") return raw;
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function unwrapEnvelope(raw: unknown) {
const record = asRecord(raw);
if (!record) return raw;
const payload = asRecord(record.payload);
if (!payload) return raw;
const hasMeta =
raw.schemaVersion !== undefined ||
raw.machineId !== undefined ||
raw.tsMs !== undefined ||
raw.tsDevice !== undefined ||
raw.seq !== undefined ||
raw.type !== undefined;
record.schemaVersion !== undefined ||
record.machineId !== undefined ||
record.tsMs !== undefined ||
record.tsDevice !== undefined ||
record.seq !== undefined ||
record.type !== undefined;
if (!hasMeta) return raw;
return {
...payload,
machineId: raw.machineId ?? payload.machineId,
tsMs: raw.tsMs ?? payload.tsMs,
tsDevice: raw.tsDevice ?? payload.tsDevice,
schemaVersion: raw.schemaVersion ?? payload.schemaVersion,
seq: raw.seq ?? payload.seq,
machineId: record.machineId ?? payload.machineId,
tsMs: record.tsMs ?? payload.tsMs,
tsDevice: record.tsDevice ?? payload.tsDevice,
schemaVersion: record.schemaVersion ?? payload.schemaVersion,
seq: record.seq ?? payload.seq,
};
}
@@ -61,10 +67,14 @@ 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 });
let body = await req.json().catch(() => null);
let body: unknown = await req.json().catch(() => null);
body = unwrapEnvelope(body);
const bodyRecord = asRecord(body) ?? {};
const machineId = body?.machineId ?? body?.machine_id ?? body?.machine?.id;
const machineId =
bodyRecord.machineId ??
bodyRecord.machine_id ??
(asRecord(bodyRecord.machine)?.id ?? null);
if (!machineId || !machineIdSchema.safeParse(String(machineId)).success) {
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
}
@@ -72,8 +82,7 @@ export async function POST(req: Request) {
const machine = await getMachineAuth(String(machineId), apiKey);
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
const raw = body as any;
const cyclesRaw = raw?.cycles ?? raw?.cycle;
const cyclesRaw = bodyRecord.cycles ?? bodyRecord.cycle;
if (!cyclesRaw) {
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
}
@@ -85,8 +94,8 @@ export async function POST(req: Request) {
}
const fallbackTsMs =
(typeof raw?.tsMs === "number" && raw.tsMs) ||
(typeof raw?.tsDevice === "number" && raw.tsDevice) ||
(typeof bodyRecord.tsMs === "number" && bodyRecord.tsMs) ||
(typeof bodyRecord.tsDevice === "number" && bodyRecord.tsDevice) ||
undefined;
const rows = parsedCycles.data.map((data) => {

View File

@@ -3,13 +3,19 @@ import { prisma } from "@/lib/prisma";
import { getMachineAuth } from "@/lib/machineAuthCache";
import { z } from "zod";
import { evaluateAlertsForEvent } from "@/lib/alerts/engine";
import { toJsonValue } from "@/lib/prismaJson";
const normalizeType = (t: any) =>
const normalizeType = (t: unknown) =>
String(t ?? "")
.trim()
.toLowerCase()
.replace(/_/g, "-");
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
const CANON_TYPE: Record<string, string> = {
// Node-RED
"production-stopped": "stop",
@@ -56,29 +62,35 @@ 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 });
let body: any = await req.json().catch(() => null);
let body: unknown = await req.json().catch(() => null);
// ✅ if Node-RED sent an array as the whole body, unwrap it
if (Array.isArray(body)) body = body[0];
const bodyRecord = asRecord(body) ?? {};
const payloadRecord = asRecord(bodyRecord.payload) ?? {};
// ✅ accept multiple common keys
const machineId = body?.machineId ?? body?.machine_id ?? body?.machine?.id;
const machineId =
bodyRecord.machineId ??
bodyRecord.machine_id ??
(asRecord(bodyRecord.machine)?.id ?? null);
let rawEvent =
body?.event ??
body?.events ??
body?.anomalies ??
body?.payload?.event ??
body?.payload?.events ??
body?.payload?.anomalies ??
body?.payload ??
body?.data; // sometimes "data"
bodyRecord.event ??
bodyRecord.events ??
bodyRecord.anomalies ??
payloadRecord.event ??
payloadRecord.events ??
payloadRecord.anomalies ??
payloadRecord ??
bodyRecord.data; // sometimes "data"
if (rawEvent?.event && typeof rawEvent.event === "object") rawEvent = rawEvent.event;
if (Array.isArray(rawEvent?.events)) rawEvent = rawEvent.events;
const rawEventRecord = asRecord(rawEvent);
if (rawEventRecord?.event && typeof rawEventRecord.event === "object") rawEvent = rawEventRecord.event;
if (Array.isArray(rawEventRecord?.events)) rawEvent = rawEventRecord.events;
if (!machineId || !rawEvent) {
return NextResponse.json(
{ ok: false, error: "Invalid payload", got: { hasMachineId: !!machineId, keys: Object.keys(body ?? {}) } },
{ ok: false, error: "Invalid payload", got: { hasMachineId: !!machineId, keys: Object.keys(bodyRecord) } },
{ status: 400 }
);
}
@@ -108,55 +120,50 @@ export async function POST(req: Request) {
}
const created: { id: string; ts: Date; eventType: string }[] = [];
const skipped: any[] = [];
const skipped: Array<Record<string, unknown>> = [];
for (const ev of events) {
if (!ev || typeof ev !== "object") {
const evRecord = asRecord(ev);
if (!evRecord) {
skipped.push({ reason: "invalid_event_object" });
continue;
}
const evData = asRecord(evRecord.data) ?? {};
const rawType = (ev as any).eventType ?? (ev as any).anomaly_type ?? (ev as any).topic ?? body.topic ?? "";
const rawType = evRecord.eventType ?? evRecord.anomaly_type ?? evRecord.topic ?? bodyRecord.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) ||
(typeof evRecord.timestamp === "number" && evRecord.timestamp) ||
(typeof evData.timestamp === "number" && evData.timestamp) ||
(typeof evData.event_timestamp === "number" && evData.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();
let sev = String(evRecord.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) ||
(typeof evData.stoppage_duration_seconds === "number" && evData.stoppage_duration_seconds) ||
(typeof evData.stop_duration_seconds === "number" && evData.stop_duration_seconds) ||
null;
if (stopSec != null) {
const theoretical =
Number(
(ev as any)?.data?.theoretical_cycle_time ??
(ev as any)?.data?.theoreticalCycleTime ??
0
) || 0;
const theoretical = Number(evData.theoretical_cycle_time ?? evData.theoreticalCycleTime ?? 0) || 0;
const microMultiplier = Number(
(ev as any)?.data?.micro_threshold_multiplier ??
(ev as any)?.data?.threshold_multiplier ??
defaultMicroMultiplier
evData.micro_threshold_multiplier ?? evData.threshold_multiplier ?? defaultMicroMultiplier
);
const macroMultiplier = Math.max(
microMultiplier,
Number((ev as any)?.data?.macro_threshold_multiplier ?? defaultMacroMultiplier)
Number(evData.macro_threshold_multiplier ?? defaultMacroMultiplier)
);
if (theoretical > 0) {
@@ -177,39 +184,60 @@ export async function POST(req: Request) {
}
const title =
clampText((ev as any).title, 160) ||
clampText(evRecord.title, 160) ||
(finalType === "slow-cycle" ? "Slow Cycle Detected" :
finalType === "macrostop" ? "Macrostop Detected" :
finalType === "microstop" ? "Microstop Detected" :
"Event");
const description = clampText((ev as any).description, 1000);
const description = clampText(evRecord.description, 1000);
// 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 rawData = evRecord.data ?? evRecord;
const parsedData = typeof rawData === "string"
? (() => {
try {
return JSON.parse(rawData);
} catch {
return { raw: rawData };
}
})()
: rawData;
const dataObj: Record<string, unknown> =
parsedData && typeof parsedData === "object" && !Array.isArray(parsedData)
? { ...(parsedData as Record<string, unknown>) }
: { raw: parsedData };
if (evRecord.status != null && dataObj.status == null) dataObj.status = evRecord.status;
if (evRecord.alert_id != null && dataObj.alert_id == null) dataObj.alert_id = evRecord.alert_id;
if (evRecord.is_update != null && dataObj.is_update == null) dataObj.is_update = evRecord.is_update;
if (evRecord.is_auto_ack != null && dataObj.is_auto_ack == null) dataObj.is_auto_ack = evRecord.is_auto_ack;
const activeWorkOrder = asRecord(evRecord.activeWorkOrder);
const dataActiveWorkOrder = asRecord(evData.activeWorkOrder);
const row = await prisma.machineEvent.create({
data: {
orgId: machine.orgId,
machineId: machine.id,
ts,
topic: clampText((ev as any).topic ?? finalType, 64) ?? finalType,
topic: clampText(evRecord.topic ?? finalType, 64) ?? finalType,
eventType: finalType,
severity: sev,
requiresAck: !!(ev as any).requires_ack,
requiresAck: !!evRecord.requires_ack,
title,
description,
data: dataObj,
data: toJsonValue(dataObj),
workOrderId:
clampText((ev as any)?.work_order_id, 64) ??
clampText((ev as any)?.data?.work_order_id, 64) ??
clampText(evRecord.work_order_id, 64) ??
clampText(evData.work_order_id, 64) ??
clampText(activeWorkOrder?.id, 64) ??
clampText(dataActiveWorkOrder?.id, 64) ??
null,
sku:
clampText((ev as any)?.sku, 64) ??
clampText((ev as any)?.data?.sku, 64) ??
clampText(evRecord.sku, 64) ??
clampText(evData.sku, 64) ??
clampText(activeWorkOrder?.sku, 64) ??
clampText(dataActiveWorkOrder?.sku, 64) ??
null,
},
});

View File

@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getMachineAuth } from "@/lib/machineAuthCache";
import { normalizeHeartbeatV1 } from "@/lib/contracts/v1";
import { toJsonValue } from "@/lib/prismaJson";
function getClientIp(req: Request) {
const xf = req.headers.get("x-forwarded-for");
@@ -24,7 +25,7 @@ export async function POST(req: Request) {
const ip = getClientIp(req);
const userAgent = req.headers.get("user-agent");
let rawBody: any = null;
let rawBody: unknown = null;
let orgId: string | null = null;
let machineId: string | null = null;
let seq: bigint | null = null;
@@ -48,7 +49,16 @@ export async function POST(req: Request) {
const normalized = normalizeHeartbeatV1(rawBody);
if (!normalized.ok) {
await prisma.ingestLog.create({
data: { endpoint, ok: false, status: 400, errorCode: "INVALID_PAYLOAD", errorMsg: normalized.error, body: rawBody, ip, userAgent },
data: {
endpoint,
ok: false,
status: 400,
errorCode: "INVALID_PAYLOAD",
errorMsg: normalized.error,
body: toJsonValue(rawBody),
ip,
userAgent,
},
});
return NextResponse.json({ ok: false, error: "Invalid payload", detail: normalized.error }, { status: 400 });
}
@@ -70,7 +80,7 @@ export async function POST(req: Request) {
status: 401,
errorCode: "UNAUTHORIZED",
errorMsg: "Unauthorized (machineId/apiKey mismatch)",
body: rawBody,
body: toJsonValue(rawBody),
machineId,
schemaVersion,
seq,
@@ -123,8 +133,8 @@ export async function POST(req: Request) {
tsDevice: hb.ts,
tsServer: hb.tsServer,
});
} catch (err: any) {
const msg = err?.message ? String(err.message) : "Unknown error";
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Unknown error";
try {
await prisma.ingestLog.create({
@@ -139,7 +149,7 @@ export async function POST(req: Request) {
schemaVersion,
seq,
tsDevice: tsDeviceDate ?? undefined,
body: rawBody,
body: toJsonValue(rawBody),
ip,
userAgent,
},

View File

@@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getMachineAuth } from "@/lib/machineAuthCache";
import { normalizeSnapshotV1 } from "@/lib/contracts/v1";
import { toJsonValue } from "@/lib/prismaJson";
function getClientIp(req: Request) {
const xf = req.headers.get("x-forwarded-for");
@@ -26,7 +27,7 @@ export async function POST(req: Request) {
const ip = getClientIp(req);
const userAgent = req.headers.get("user-agent");
let rawBody: any = null;
let rawBody: unknown = null;
let orgId: string | null = null;
let machineId: string | null = null;
let seq: bigint | null = null;
@@ -60,7 +61,7 @@ export async function POST(req: Request) {
status: 400,
errorCode: "INVALID_PAYLOAD",
errorMsg: normalized.error,
body: rawBody,
body: toJsonValue(rawBody),
ip,
userAgent,
},
@@ -86,7 +87,7 @@ export async function POST(req: Request) {
status: 401,
errorCode: "UNAUTHORIZED",
errorMsg: "Unauthorized (machineId/apiKey mismatch)",
body: rawBody,
body: toJsonValue(rawBody),
machineId,
schemaVersion,
seq,
@@ -100,19 +101,37 @@ export async function POST(req: Request) {
orgId = machine.orgId;
const wo = body.activeWorkOrder ?? {};
const good = typeof wo.good === "number" ? wo.good : (typeof wo.goodParts === "number" ? wo.goodParts : null);
const scrap = typeof wo.scrap === "number" ? wo.scrap : (typeof wo.scrapParts === "number" ? wo.scrapParts : null)
const woRecord = (body.activeWorkOrder ?? {}) as Record<string, unknown>;
const good =
typeof woRecord.good === "number"
? woRecord.good
: typeof woRecord.goodParts === "number"
? woRecord.goodParts
: typeof woRecord.good_parts === "number"
? woRecord.good_parts
: null;
const scrap =
typeof woRecord.scrap === "number"
? woRecord.scrap
: typeof woRecord.scrapParts === "number"
? woRecord.scrapParts
: typeof woRecord.scrap_parts === "number"
? woRecord.scrap_parts
: null;
const k = body.kpis ?? {};
const safeCycleTime =
typeof body.cycleTime === "number" && body.cycleTime > 0
? body.cycleTime
: (typeof (wo as any).cycleTime === "number" && (wo as any).cycleTime > 0 ? (wo as any).cycleTime : null);
typeof body.cycleTime === "number" && body.cycleTime > 0
? body.cycleTime
: typeof woRecord.cycleTime === "number" && woRecord.cycleTime > 0
? woRecord.cycleTime
: null;
const safeCavities =
typeof body.cavities === "number" && body.cavities > 0
? body.cavities
: (typeof (wo as any).cavities === "number" && (wo as any).cavities > 0 ? (wo as any).cavities : null);
const safeCavities =
typeof body.cavities === "number" && body.cavities > 0
? body.cavities
: typeof woRecord.cavities === "number" && woRecord.cavities > 0
? woRecord.cavities
: null;
// Write snapshot (ts = tsDevice; tsServer auto)
const row = await prisma.machineKpiSnapshot.create({
data: {
@@ -125,9 +144,9 @@ export async function POST(req: Request) {
ts: tsDeviceDate, // store device-time in ts; server-time goes to ts_server
// Work order fields
workOrderId: wo.id ? String(wo.id) : null,
sku: wo.sku ? String(wo.sku) : null,
target: typeof wo.target === "number" ? Math.trunc(wo.target) : null,
workOrderId: woRecord.id != null ? String(woRecord.id) : null,
sku: woRecord.sku != null ? String(woRecord.sku) : null,
target: typeof woRecord.target === "number" ? Math.trunc(woRecord.target) : null,
good: good != null ? Math.trunc(good) : null,
scrap: scrap != null ? Math.trunc(scrap) : null,
@@ -169,8 +188,8 @@ export async function POST(req: Request) {
tsDevice: row.ts,
tsServer: row.tsServer,
});
} catch (err: any) {
const msg = err?.message ? String(err.message) : "Unknown error";
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Unknown error";
// Never fail the request because logging failed
try {
@@ -186,7 +205,7 @@ export async function POST(req: Request) {
schemaVersion,
seq,
tsDevice: tsDeviceDate ?? undefined,
body: rawBody,
body: toJsonValue(rawBody),
ip,
userAgent,
},