Files
MIS-Contro-Tower/lib/events/normalizeEvent.ts

215 lines
7.3 KiB
TypeScript

type NormalizeThresholds = {
microMultiplier: number;
macroMultiplier: number;
};
type RawEventRow = {
id: string;
ts?: Date | null;
topic?: string | null;
eventType?: string | null;
severity?: string | null;
title?: string | null;
description?: string | null;
requiresAck?: boolean | null;
data?: unknown;
workOrderId?: string | null;
};
function coerceString(value: unknown) {
if (value === null || value === undefined) return null;
if (typeof value === "string") return value;
if (typeof value === "number") return String(value);
return null;
}
export function normalizeEvent(row: RawEventRow, thresholds: NormalizeThresholds) {
// -----------------------------
// 1) Parse row.data safely
// data may be:
// - object
// - array of objects
// - JSON string of either
// -----------------------------
const raw = row.data;
let parsed: unknown = raw;
if (typeof raw === "string") {
try {
parsed = JSON.parse(raw);
} catch {
parsed = raw; // keep as string if not JSON
}
}
// data can be object OR [object]
const blob = Array.isArray(parsed) ? parsed[0] : parsed;
// some payloads nest details under blob.data
const inner = (blob as { data?: unknown })?.data ?? blob ?? {};
const normalizeType = (t: unknown) =>
String(t ?? "")
.trim()
.toLowerCase()
.replace(/_/g, "-");
// -----------------------------
// 2) Alias mapping (canonical types)
// -----------------------------
const ALIAS: Record<string, string> = {
// Spanish / synonyms
macroparo: "macrostop",
"macro-stop": "macrostop",
macro_stop: "macrostop",
microparo: "microstop",
"micro-paro": "microstop",
micro_stop: "microstop",
// Node-RED types
"production-stopped": "stop", // we'll classify to micro/macro below
// legacy / generic
down: "stop",
};
// -----------------------------
// 3) Determine event type from DB or blob
// -----------------------------
const fromDbType = row.eventType && row.eventType !== "unknown" ? row.eventType : null;
const fromBlobType =
(blob as { anomaly_type?: unknown; eventType?: unknown; topic?: unknown })?.anomaly_type ??
(blob as { anomaly_type?: unknown; eventType?: unknown; topic?: unknown })?.eventType ??
(blob as { anomaly_type?: unknown; eventType?: unknown; topic?: unknown })?.topic ??
(inner as { anomaly_type?: unknown; eventType?: unknown })?.anomaly_type ??
(inner as { anomaly_type?: unknown; eventType?: unknown })?.eventType ??
null;
// infer slow-cycle if signature exists
const inferredType =
fromDbType ??
fromBlobType ??
(((inner as { actual_cycle_time?: unknown; theoretical_cycle_time?: unknown })?.actual_cycle_time &&
(inner as { actual_cycle_time?: unknown; theoretical_cycle_time?: unknown })?.theoretical_cycle_time) ||
((blob as { actual_cycle_time?: unknown; theoretical_cycle_time?: unknown })?.actual_cycle_time &&
(blob as { actual_cycle_time?: unknown; theoretical_cycle_time?: unknown })?.theoretical_cycle_time)
? "slow-cycle"
: "unknown");
const eventTypeRaw = normalizeType(inferredType);
let eventType = ALIAS[eventTypeRaw] ?? eventTypeRaw;
// -----------------------------
// 4) Optional: classify "stop" into micro/macro based on duration if present
// (keeps old rows usable even if they stored production-stopped)
// -----------------------------
if (eventType === "stop") {
const innerData = inner as {
stoppage_duration_seconds?: unknown;
stop_duration_seconds?: unknown;
theoretical_cycle_time?: unknown;
};
const blobData = blob as {
stoppage_duration_seconds?: unknown;
stop_duration_seconds?: unknown;
theoretical_cycle_time?: unknown;
};
const stopSec =
(typeof innerData?.stoppage_duration_seconds === "number" && innerData.stoppage_duration_seconds) ||
(typeof blobData?.stoppage_duration_seconds === "number" && blobData.stoppage_duration_seconds) ||
(typeof innerData?.stop_duration_seconds === "number" && innerData.stop_duration_seconds) ||
null;
const microMultiplier = Number(thresholds?.microMultiplier ?? 1.5);
const macroMultiplier = Math.max(microMultiplier, Number(thresholds?.macroMultiplier ?? 5));
const theoreticalCycle =
Number(innerData?.theoretical_cycle_time ?? blobData?.theoretical_cycle_time) || 0;
if (stopSec != null) {
if (theoreticalCycle > 0) {
const macroThresholdSec = theoreticalCycle * macroMultiplier;
eventType = stopSec >= macroThresholdSec ? "macrostop" : "microstop";
} else {
const fallbackMacroSec = 300;
eventType = stopSec >= fallbackMacroSec ? "macrostop" : "microstop";
}
}
}
// -----------------------------
// 5) Severity, title, description, timestamp
// -----------------------------
const severity =
String(
(row.severity && row.severity !== "info" ? row.severity : null) ??
(blob as { severity?: unknown })?.severity ??
(inner as { severity?: unknown })?.severity ??
"info"
)
.trim()
.toLowerCase();
const title =
String(
(row.title && row.title !== "Event" ? row.title : null) ??
(blob as { title?: unknown })?.title ??
(inner as { title?: unknown })?.title ??
(eventType === "slow-cycle" ? "Slow Cycle Detected" : "Event")
).trim();
const description =
row.description ??
(blob as { description?: string | null })?.description ??
(inner as { description?: string | null })?.description ??
(eventType === "slow-cycle" &&
((inner as { actual_cycle_time?: unknown })?.actual_cycle_time ??
(blob as { actual_cycle_time?: unknown })?.actual_cycle_time) &&
((inner as { theoretical_cycle_time?: unknown })?.theoretical_cycle_time ??
(blob as { theoretical_cycle_time?: unknown })?.theoretical_cycle_time) &&
((inner as { delta_percent?: unknown })?.delta_percent ??
(blob as { delta_percent?: unknown })?.delta_percent) != null
? `Cycle took ${Number(
(inner as { actual_cycle_time?: unknown })?.actual_cycle_time ??
(blob as { actual_cycle_time?: unknown })?.actual_cycle_time
).toFixed(1)}s (+${Number(
(inner as { delta_percent?: unknown })?.delta_percent ??
(blob as { delta_percent?: unknown })?.delta_percent
)}% vs ${Number(
(inner as { theoretical_cycle_time?: unknown })?.theoretical_cycle_time ??
(blob as { theoretical_cycle_time?: unknown })?.theoretical_cycle_time
).toFixed(1)}s objetivo)`
: null);
const ts =
row.ts ??
(typeof (blob as { timestamp?: unknown })?.timestamp === "number"
? new Date((blob as { timestamp?: number }).timestamp as number)
: null) ??
(typeof (inner as { timestamp?: unknown })?.timestamp === "number"
? new Date((inner as { timestamp?: number }).timestamp as number)
: null) ??
null;
const workOrderId =
coerceString(row.workOrderId) ??
coerceString((blob as { work_order_id?: unknown })?.work_order_id) ??
coerceString((inner as { work_order_id?: unknown })?.work_order_id) ??
null;
return {
id: row.id,
ts,
topic: String(row.topic ?? (blob as { topic?: unknown })?.topic ?? eventType),
eventType,
severity,
title,
description,
requiresAck: !!row.requiresAck,
workOrderId,
};
}