pre-bemis
This commit is contained in:
@@ -33,6 +33,48 @@ function unwrapEnvelope(raw: unknown) {
|
||||
};
|
||||
}
|
||||
|
||||
function asNumber(value: unknown) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeCycleInput(raw: unknown): Record<string, unknown> | null {
|
||||
const row = asRecord(raw);
|
||||
if (!row) return null;
|
||||
const data = asRecord(row.data);
|
||||
|
||||
const fromRowOrData = (keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
if (row[key] !== undefined) return row[key];
|
||||
if (data && data[key] !== undefined) return data[key];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
...row,
|
||||
actual_cycle_time: fromRowOrData(["actual_cycle_time", "actualCycleTime", "actual_cycle", "actual"]),
|
||||
theoretical_cycle_time: fromRowOrData([
|
||||
"theoretical_cycle_time",
|
||||
"theoreticalCycleTime",
|
||||
"cycleTime",
|
||||
"cycle_time",
|
||||
"ideal",
|
||||
]),
|
||||
cycle_count: fromRowOrData(["cycle_count", "cycleCount"]),
|
||||
work_order_id: fromRowOrData(["work_order_id", "workOrderId"]),
|
||||
good_delta: fromRowOrData(["good_delta", "goodDelta"]),
|
||||
scrap_delta: fromRowOrData(["scrap_delta", "scrapDelta", "scrap_total"]),
|
||||
timestamp: fromRowOrData(["timestamp", "tsMs"]),
|
||||
ts: fromRowOrData(["ts", "tsMs"]),
|
||||
event_timestamp: fromRowOrData(["event_timestamp", "eventTimestamp"]),
|
||||
};
|
||||
}
|
||||
|
||||
const numberFromAny = z.preprocess((value) => {
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value === "string" && value.trim() !== "") return Number(value);
|
||||
@@ -87,15 +129,22 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
|
||||
}
|
||||
|
||||
const cycleList = Array.isArray(cyclesRaw) ? cyclesRaw : [cyclesRaw];
|
||||
const cycleList = (Array.isArray(cyclesRaw) ? cyclesRaw : [cyclesRaw])
|
||||
.map((row) => normalizeCycleInput(row))
|
||||
.filter((row): row is Record<string, unknown> => !!row);
|
||||
|
||||
if (!cycleList.length) {
|
||||
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsedCycles = z.array(cycleSchema).safeParse(cycleList);
|
||||
if (!parsedCycles.success) {
|
||||
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
|
||||
}
|
||||
|
||||
const fallbackTsMs =
|
||||
(typeof bodyRecord.tsMs === "number" && bodyRecord.tsMs) ||
|
||||
(typeof bodyRecord.tsDevice === "number" && bodyRecord.tsDevice) ||
|
||||
asNumber(bodyRecord.tsMs) ||
|
||||
asNumber(bodyRecord.tsDevice) ||
|
||||
undefined;
|
||||
|
||||
const rows = parsedCycles.data.map((data) => {
|
||||
|
||||
@@ -4,6 +4,14 @@ import { getMachineAuth } from "@/lib/machineAuthCache";
|
||||
import { z } from "zod";
|
||||
import { evaluateAlertsForEvent } from "@/lib/alerts/engine";
|
||||
import { toJsonValue } from "@/lib/prismaJson";
|
||||
import {
|
||||
findCatalogReason,
|
||||
loadFallbackReasonCatalog,
|
||||
normalizeReasonCatalog,
|
||||
toReasonCode,
|
||||
type ReasonCatalog,
|
||||
type ReasonCatalogKind,
|
||||
} from "@/lib/reasonCatalog";
|
||||
|
||||
const normalizeType = (t: unknown) =>
|
||||
String(t ?? "")
|
||||
@@ -30,6 +38,8 @@ const CANON_TYPE: Record<string, string> = {
|
||||
"microparo": "microstop",
|
||||
"micro-paro": "microstop",
|
||||
"down": "stop",
|
||||
"downtime-acknowledged": "downtime-acknowledged",
|
||||
"scrap-manual-entry": "scrap-manual-entry",
|
||||
};
|
||||
|
||||
const ALLOWED_TYPES = new Set([
|
||||
@@ -42,6 +52,8 @@ const ALLOWED_TYPES = new Set([
|
||||
"quality-spike",
|
||||
"performance-degradation",
|
||||
"predictive-oee-decline",
|
||||
"downtime-acknowledged",
|
||||
"scrap-manual-entry",
|
||||
]);
|
||||
|
||||
const machineIdSchema = z.string().uuid();
|
||||
@@ -58,6 +70,153 @@ function clampText(value: unknown, maxLen: number) {
|
||||
return text.length > maxLen ? text.slice(0, maxLen) : text;
|
||||
}
|
||||
|
||||
function numberFrom(value: unknown) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function canonicalText(value: unknown) {
|
||||
return String(value ?? "")
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
function parseReasonPath(rawPath: unknown) {
|
||||
let category: string | null = null;
|
||||
let detail: string | null = null;
|
||||
|
||||
if (Array.isArray(rawPath)) {
|
||||
const first = rawPath[0];
|
||||
const second = rawPath[1];
|
||||
if (typeof first === "string") category = first;
|
||||
if (typeof second === "string") detail = second;
|
||||
if (asRecord(first)) category = clampText(first.id ?? first.label ?? first.value, 120);
|
||||
if (asRecord(second)) detail = clampText(second.id ?? second.label ?? second.value, 120);
|
||||
} else if (typeof rawPath === "string") {
|
||||
const pieces = rawPath
|
||||
.split(/>|\/|\\|\|/g)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
category = pieces[0] ?? null;
|
||||
detail = pieces[1] ?? null;
|
||||
}
|
||||
|
||||
return {
|
||||
category: clampText(category, 120),
|
||||
detail: clampText(detail, 120),
|
||||
};
|
||||
}
|
||||
|
||||
function parseReasonTextPath(reasonText: unknown) {
|
||||
const text = clampText(reasonText, 240);
|
||||
if (!text) return { category: null as string | null, detail: null as string | null };
|
||||
const pieces = text
|
||||
.split(/>|\/|\\|\|/g)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
return {
|
||||
category: clampText(pieces[0] ?? null, 120),
|
||||
detail: clampText(pieces[1] ?? null, 120),
|
||||
};
|
||||
}
|
||||
|
||||
function findCatalogReasonFlexible(
|
||||
catalog: ReasonCatalog | null,
|
||||
kind: ReasonCatalogKind,
|
||||
categoryIdOrLabel: unknown,
|
||||
detailIdOrLabel: unknown
|
||||
) {
|
||||
const direct = findCatalogReason(catalog, kind, categoryIdOrLabel, detailIdOrLabel);
|
||||
if (direct) return direct;
|
||||
if (!catalog) return null;
|
||||
|
||||
const catNeedle = canonicalText(categoryIdOrLabel);
|
||||
const detNeedle = canonicalText(detailIdOrLabel);
|
||||
if (!catNeedle || !detNeedle) return null;
|
||||
|
||||
for (const category of catalog[kind] ?? []) {
|
||||
const catMatch =
|
||||
canonicalText(category.id) === catNeedle || canonicalText(category.label) === catNeedle;
|
||||
if (!catMatch) continue;
|
||||
for (const detail of category.details) {
|
||||
const detMatch = canonicalText(detail.id) === detNeedle || canonicalText(detail.label) === detNeedle;
|
||||
if (!detMatch) continue;
|
||||
return {
|
||||
categoryId: category.id,
|
||||
categoryLabel: category.label,
|
||||
detailId: detail.id,
|
||||
detailLabel: detail.label,
|
||||
reasonCode: toReasonCode(category.id, detail.id),
|
||||
reasonLabel: `${category.label} > ${detail.label}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCatalogFromDefaults(defaultsJson: unknown) {
|
||||
const defaults = asRecord(defaultsJson);
|
||||
if (!defaults) return null;
|
||||
return normalizeReasonCatalog(defaults.reasonCatalog ?? defaults.reasonCatalogData);
|
||||
}
|
||||
|
||||
function resolveReason(
|
||||
raw: Record<string, unknown>,
|
||||
kind: ReasonCatalogKind,
|
||||
catalog: ReasonCatalog | null,
|
||||
fallbackVersion: number
|
||||
) {
|
||||
const reasonPath = parseReasonPath(raw.reasonPath);
|
||||
const reasonTextPath = parseReasonTextPath(raw.reasonText);
|
||||
const categoryIdRaw = clampText(raw.categoryId ?? reasonPath.category ?? reasonTextPath.category, 64);
|
||||
const detailIdRaw = clampText(raw.detailId ?? reasonPath.detail ?? reasonTextPath.detail, 64);
|
||||
const fromCatalog = findCatalogReasonFlexible(catalog, kind, categoryIdRaw, detailIdRaw);
|
||||
|
||||
const categoryLabelRaw = clampText(raw.categoryLabel ?? reasonPath.category ?? reasonTextPath.category, 120);
|
||||
const detailLabelRaw = clampText(raw.detailLabel ?? reasonPath.detail ?? reasonTextPath.detail, 120);
|
||||
|
||||
const reasonCode =
|
||||
clampText(raw.reasonCode, 64)?.toUpperCase() ??
|
||||
fromCatalog?.reasonCode ??
|
||||
toReasonCode(categoryIdRaw ?? categoryLabelRaw, detailIdRaw ?? detailLabelRaw) ??
|
||||
null;
|
||||
|
||||
const categoryId = fromCatalog?.categoryId ?? categoryIdRaw;
|
||||
const detailId = fromCatalog?.detailId ?? detailIdRaw;
|
||||
const categoryLabel = fromCatalog?.categoryLabel ?? categoryLabelRaw;
|
||||
const detailLabel = fromCatalog?.detailLabel ?? detailLabelRaw;
|
||||
|
||||
const pathLabel =
|
||||
clampText(raw.reasonText, 240) ??
|
||||
fromCatalog?.reasonLabel ??
|
||||
(categoryLabel && detailLabel ? `${categoryLabel} > ${detailLabel}` : null) ??
|
||||
detailLabel ??
|
||||
categoryLabel ??
|
||||
reasonCode;
|
||||
|
||||
const catalogVersionRaw = numberFrom(raw.catalogVersion);
|
||||
const catalogVersion = catalogVersionRaw != null ? Math.trunc(catalogVersionRaw) : fallbackVersion;
|
||||
|
||||
return {
|
||||
type: kind,
|
||||
categoryId,
|
||||
categoryLabel,
|
||||
detailId,
|
||||
detailLabel,
|
||||
reasonCode,
|
||||
reasonLabel: pathLabel,
|
||||
reasonText: pathLabel,
|
||||
catalogVersion,
|
||||
};
|
||||
}
|
||||
|
||||
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 });
|
||||
@@ -103,8 +262,11 @@ export async function POST(req: Request) {
|
||||
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||
const orgSettings = await prisma.orgSettings.findUnique({
|
||||
where: { orgId: machine.orgId },
|
||||
select: { stoppageMultiplier: true, macroStoppageMultiplier: true },
|
||||
select: { stoppageMultiplier: true, macroStoppageMultiplier: true, defaultsJson: true },
|
||||
});
|
||||
const fallbackCatalog = await loadFallbackReasonCatalog();
|
||||
const settingsCatalog = getCatalogFromDefaults(orgSettings?.defaultsJson);
|
||||
const reasonCatalog = settingsCatalog ?? fallbackCatalog;
|
||||
|
||||
const defaultMicroMultiplier = Number(orgSettings?.stoppageMultiplier ?? 1.5);
|
||||
const defaultMacroMultiplier = Math.max(
|
||||
@@ -129,6 +291,8 @@ export async function POST(req: Request) {
|
||||
continue;
|
||||
}
|
||||
const evData = asRecord(evRecord.data) ?? {};
|
||||
const evReason = asRecord(evRecord.reason) ?? asRecord(evData.reason);
|
||||
const evDowntime = asRecord(evRecord.downtime) ?? asRecord(evData.downtime);
|
||||
|
||||
const rawType = evRecord.eventType ?? evRecord.anomaly_type ?? evRecord.topic ?? bodyRecord.topic ?? "";
|
||||
const typ0 = normalizeType(rawType);
|
||||
@@ -211,6 +375,8 @@ export async function POST(req: Request) {
|
||||
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;
|
||||
if (evReason && dataObj.reason == null) dataObj.reason = evReason;
|
||||
if (evDowntime && dataObj.downtime == null) dataObj.downtime = evDowntime;
|
||||
|
||||
const activeWorkOrder = asRecord(evRecord.activeWorkOrder);
|
||||
const dataActiveWorkOrder = asRecord(evData.activeWorkOrder);
|
||||
@@ -244,8 +410,127 @@ export async function POST(req: Request) {
|
||||
|
||||
created.push({ id: row.id, ts: row.ts, eventType: row.eventType });
|
||||
|
||||
if (evReason) {
|
||||
const inferredKind: ReasonCatalogKind =
|
||||
String(evReason.type ?? "").toLowerCase() === "scrap" || finalType === "scrap-manual-entry"
|
||||
? "scrap"
|
||||
: "downtime";
|
||||
const resolved = resolveReason(evReason, inferredKind, reasonCatalog, reasonCatalog.version);
|
||||
|
||||
if (resolved.reasonCode) {
|
||||
const reasonId =
|
||||
clampText(evReason.reasonId, 128) ??
|
||||
(inferredKind === "downtime"
|
||||
? `evt:${machine.id}:downtime:${clampText(evReason.incidentKey ?? evDowntime?.incidentKey, 128) ?? row.id}`
|
||||
: `evt:${machine.id}:scrap:${clampText(evReason.scrapEntryId, 128) ?? row.id}`);
|
||||
|
||||
const workOrderId =
|
||||
clampText(evRecord.work_order_id, 64) ??
|
||||
clampText(evData.work_order_id, 64) ??
|
||||
clampText(evRecord.workOrderId, 64) ??
|
||||
null;
|
||||
|
||||
const commonWrite = {
|
||||
reasonCode: resolved.reasonCode,
|
||||
reasonLabel: resolved.reasonLabel ?? resolved.reasonCode,
|
||||
reasonText: resolved.reasonText ?? null,
|
||||
capturedAt: row.ts,
|
||||
workOrderId,
|
||||
schemaVersion: Math.max(1, Math.trunc(resolved.catalogVersion)),
|
||||
meta: toJsonValue({
|
||||
source: "ingest:event",
|
||||
eventId: row.id,
|
||||
eventType: row.eventType,
|
||||
incidentKey: clampText(evReason.incidentKey ?? evDowntime?.incidentKey, 128),
|
||||
anomalyType:
|
||||
clampText(evRecord.anomalyType, 64) ??
|
||||
clampText(evDowntime?.anomalyType, 64) ??
|
||||
clampText(evRecord.anomaly_type, 64),
|
||||
reason: {
|
||||
type: resolved.type,
|
||||
categoryId: resolved.categoryId,
|
||||
categoryLabel: resolved.categoryLabel,
|
||||
detailId: resolved.detailId,
|
||||
detailLabel: resolved.detailLabel,
|
||||
reasonText: resolved.reasonText,
|
||||
catalogVersion: resolved.catalogVersion,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
if (inferredKind === "downtime") {
|
||||
const incidentKey = clampText(evReason.incidentKey ?? evDowntime?.incidentKey, 128) ?? row.id;
|
||||
const durationSeconds =
|
||||
numberFrom(evDowntime?.durationSeconds) ??
|
||||
numberFrom(evData.stoppage_duration_seconds) ??
|
||||
numberFrom(evData.stop_duration_seconds) ??
|
||||
null;
|
||||
const episodeEndTsMs =
|
||||
numberFrom(evDowntime?.episodeEndTsMs) ??
|
||||
numberFrom(evDowntime?.acknowledgedAtMs) ??
|
||||
null;
|
||||
|
||||
await prisma.reasonEntry.upsert({
|
||||
where: { reasonId },
|
||||
create: {
|
||||
orgId: machine.orgId,
|
||||
machineId: machine.id,
|
||||
reasonId,
|
||||
kind: "downtime",
|
||||
episodeId: incidentKey,
|
||||
durationSeconds: durationSeconds != null ? Math.max(0, Math.trunc(durationSeconds)) : null,
|
||||
episodeEndTs: episodeEndTsMs != null ? new Date(episodeEndTsMs) : null,
|
||||
...commonWrite,
|
||||
},
|
||||
update: {
|
||||
kind: "downtime",
|
||||
episodeId: incidentKey,
|
||||
durationSeconds: durationSeconds != null ? Math.max(0, Math.trunc(durationSeconds)) : null,
|
||||
episodeEndTs: episodeEndTsMs != null ? new Date(episodeEndTsMs) : null,
|
||||
...commonWrite,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const scrapEntryId =
|
||||
clampText(evReason.scrapEntryId, 128) ??
|
||||
clampText(evRecord.id, 128) ??
|
||||
clampText(evRecord.eventId, 128) ??
|
||||
row.id;
|
||||
const scrapQtyRaw =
|
||||
numberFrom(evRecord.scrapDelta) ??
|
||||
numberFrom(evData.scrapDelta) ??
|
||||
numberFrom(evData.scrap_delta) ??
|
||||
0;
|
||||
const scrapQty = Math.max(0, Math.trunc(scrapQtyRaw));
|
||||
|
||||
await prisma.reasonEntry.upsert({
|
||||
where: { reasonId },
|
||||
create: {
|
||||
orgId: machine.orgId,
|
||||
machineId: machine.id,
|
||||
reasonId,
|
||||
kind: "scrap",
|
||||
scrapEntryId,
|
||||
scrapQty,
|
||||
scrapUnit: clampText(evReason.scrapUnit, 16) ?? null,
|
||||
...commonWrite,
|
||||
},
|
||||
update: {
|
||||
kind: "scrap",
|
||||
scrapEntryId,
|
||||
scrapQty,
|
||||
scrapUnit: clampText(evReason.scrapUnit, 16) ?? null,
|
||||
...commonWrite,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await evaluateAlertsForEvent(row.id);
|
||||
if (row.eventType !== "downtime-acknowledged" && row.eventType !== "scrap-manual-entry") {
|
||||
await evaluateAlertsForEvent(row.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[alerts] evaluation failed", err);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { getMachineAuth } from "@/lib/machineAuthCache";
|
||||
import { normalizeSnapshotV1 } from "@/lib/contracts/v1";
|
||||
import { toJsonValue } from "@/lib/prismaJson";
|
||||
import { logLine } from "@/lib/logger";
|
||||
|
||||
function getClientIp(req: Request) {
|
||||
const xf = req.headers.get("x-forwarded-for");
|
||||
@@ -21,11 +22,68 @@ function parseSeqToBigInt(seq: unknown): bigint | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 readPath(root: unknown, path: string[]): unknown {
|
||||
let current = root;
|
||||
for (const key of path) {
|
||||
const record = asRecord(current);
|
||||
if (!record) return undefined;
|
||||
current = record[key];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function collectQualityTrace(params: {
|
||||
rawBody: unknown;
|
||||
normalizedKpis: Record<string, unknown> | null;
|
||||
persistedQuality: number | null;
|
||||
machineId: string;
|
||||
rowId: string;
|
||||
}) {
|
||||
const { rawBody, normalizedKpis, persistedQuality, machineId, rowId } = params;
|
||||
const candidates = [
|
||||
"kpis.quality",
|
||||
"payload.kpis.quality",
|
||||
"kpi_snapshot.quality",
|
||||
"quality",
|
||||
"payload.quality",
|
||||
] as const;
|
||||
|
||||
const rawQualityCandidates: Record<string, { type: string; value: unknown }> = {};
|
||||
for (const path of candidates) {
|
||||
const value = readPath(rawBody, path.split("."));
|
||||
rawQualityCandidates[path] = {
|
||||
type: value === null ? "null" : typeof value,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedQuality = normalizedKpis?.quality;
|
||||
return {
|
||||
machineId,
|
||||
rowId,
|
||||
rawQualityCandidates,
|
||||
normalizedQuality: {
|
||||
type: normalizedQuality === null ? "null" : typeof normalizedQuality,
|
||||
value: normalizedQuality ?? null,
|
||||
},
|
||||
persistedQuality: {
|
||||
type: persistedQuality === null ? "null" : typeof persistedQuality,
|
||||
value: persistedQuality,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const endpoint = "/api/ingest/kpi";
|
||||
const startedAt = Date.now();
|
||||
const ip = getClientIp(req);
|
||||
const userAgent = req.headers.get("user-agent");
|
||||
const traceEnabled = process.env.TRACE_KPI_INGEST === "1" || req.headers.get("x-debug-ingest") === "1";
|
||||
|
||||
let rawBody: unknown = null;
|
||||
let orgId: string | null = null;
|
||||
@@ -182,11 +240,33 @@ export async function POST(req: Request) {
|
||||
},
|
||||
});
|
||||
|
||||
const trace = collectQualityTrace({
|
||||
rawBody,
|
||||
normalizedKpis: asRecord(k),
|
||||
persistedQuality: row.quality ?? null,
|
||||
machineId: machine.id,
|
||||
rowId: row.id,
|
||||
});
|
||||
if (traceEnabled) {
|
||||
logLine("ingest.kpi.trace", {
|
||||
endpoint,
|
||||
machineId: machine.id,
|
||||
orgId,
|
||||
schemaVersion,
|
||||
seq: seq != null ? seq.toString() : null,
|
||||
ip,
|
||||
userAgent,
|
||||
trace,
|
||||
rawBody: toJsonValue(rawBody),
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
id: row.id,
|
||||
tsDevice: row.ts,
|
||||
tsServer: row.tsServer,
|
||||
trace: traceEnabled ? trace : undefined,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : "Unknown error";
|
||||
|
||||
Reference in New Issue
Block a user