Resolving polling/server migration

This commit is contained in:
Marcelo
2026-01-15 18:43:21 +00:00
parent f231d87ae3
commit 9f1af71d15
15 changed files with 794 additions and 845 deletions

View File

@@ -329,7 +329,7 @@ export default function MachineDetailClient() {
}
load();
const timer = setInterval(load, 5000);
const timer = setInterval(load, 15000);
return () => {
alive = false;
clearInterval(timer);
@@ -605,7 +605,7 @@ export default function MachineDetailClient() {
<div className="mt-4 rounded-2xl border border-white/10 bg-black/25 p-4">
<div className="mb-2 flex justify-between text-[11px] text-zinc-500">
<span>0s</span>
<span>3h</span>
<span>1h</span>
</div>
<div className="flex h-14 w-full overflow-hidden rounded-2xl">

View File

@@ -81,7 +81,7 @@ export default function MachinesPage() {
}
load();
const t = setInterval(load, 5000);
const t = setInterval(load, 15000);
return () => {
alive = false;
@@ -322,4 +322,3 @@ export default function MachinesPage() {
}

View File

@@ -176,7 +176,7 @@ export default function OverviewPage() {
}
load();
const t = setInterval(load, 15000);
const t = setInterval(load, 30000);
return () => {
alive = false;
clearInterval(t);

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getMachineAuth } from "@/lib/machineAuthCache";
import { z } from "zod";
function unwrapEnvelope(raw: any) {
@@ -38,24 +39,21 @@ const intFromAny = z.preprocess((value) => {
return value;
}, z.number().int().finite());
const cyclePayloadSchema = z
const machineIdSchema = z.string().uuid();
const cycleSchema = z
.object({
machineId: z.string().uuid(),
cycle: z
.object({
actual_cycle_time: numberFromAny,
theoretical_cycle_time: numberFromAny.optional(),
cycle_count: intFromAny.optional(),
work_order_id: z.string().trim().max(64).optional(),
sku: z.string().trim().max(64).optional(),
cavities: intFromAny.optional(),
good_delta: intFromAny.optional(),
scrap_delta: intFromAny.optional(),
timestamp: numberFromAny.optional(),
ts: numberFromAny.optional(),
event_timestamp: numberFromAny.optional(),
})
.passthrough(),
actual_cycle_time: numberFromAny,
theoretical_cycle_time: numberFromAny.optional(),
cycle_count: intFromAny.optional(),
work_order_id: z.string().trim().max(64).optional(),
sku: z.string().trim().max(64).optional(),
cavities: intFromAny.optional(),
good_delta: intFromAny.optional(),
scrap_delta: intFromAny.optional(),
timestamp: numberFromAny.optional(),
ts: numberFromAny.optional(),
event_timestamp: numberFromAny.optional(),
})
.passthrough();
@@ -66,44 +64,60 @@ export async function POST(req: Request) {
let body = await req.json().catch(() => null);
body = unwrapEnvelope(body);
const parsed = cyclePayloadSchema.safeParse(body);
if (!parsed.success) {
const machineId = body?.machineId ?? body?.machine_id ?? body?.machine?.id;
if (!machineId || !machineIdSchema.safeParse(String(machineId)).success) {
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
}
const machine = await prisma.machine.findFirst({
where: { id: parsed.data.machineId, apiKey },
select: { id: true, orgId: true },
});
const machine = await getMachineAuth(String(machineId), apiKey);
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
const c = parsed.data.cycle;
const raw = body as any;
const cyclesRaw = raw?.cycles ?? raw?.cycle;
if (!cyclesRaw) {
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
}
const tsMs =
(typeof c.timestamp === "number" && c.timestamp) ||
(typeof c.ts === "number" && c.ts) ||
(typeof c.event_timestamp === "number" && c.event_timestamp) ||
const cycleList = Array.isArray(cyclesRaw) ? cyclesRaw : [cyclesRaw];
const parsedCycles = z.array(cycleSchema).safeParse(cycleList);
if (!parsedCycles.success) {
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
}
const fallbackTsMs =
(typeof raw?.tsMs === "number" && raw.tsMs) ||
(typeof raw?.tsDevice === "number" && raw.tsDevice) ||
undefined;
const ts = tsMs ? new Date(tsMs) : new Date();
const rows = parsedCycles.data.map((data) => {
const tsMs =
(typeof data.timestamp === "number" && data.timestamp) ||
(typeof data.ts === "number" && data.ts) ||
(typeof data.event_timestamp === "number" && data.event_timestamp) ||
fallbackTsMs;
const row = await prisma.machineCycle.create({
data: {
const ts = tsMs ? new Date(tsMs) : new Date();
return {
orgId: machine.orgId,
machineId: machine.id,
ts,
cycleCount: typeof c.cycle_count === "number" ? c.cycle_count : null,
actualCycleTime: c.actual_cycle_time,
theoreticalCycleTime: typeof c.theoretical_cycle_time === "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,
},
cycleCount: typeof data.cycle_count === "number" ? data.cycle_count : null,
actualCycleTime: data.actual_cycle_time,
theoreticalCycleTime: typeof data.theoretical_cycle_time === "number" ? data.theoretical_cycle_time : null,
workOrderId: data.work_order_id ? String(data.work_order_id) : null,
sku: data.sku ? String(data.sku) : null,
cavities: typeof data.cavities === "number" ? data.cavities : null,
goodDelta: typeof data.good_delta === "number" ? data.good_delta : null,
scrapDelta: typeof data.scrap_delta === "number" ? data.scrap_delta : null,
};
});
return NextResponse.json({ ok: true, id: row.id, ts: row.ts });
if (rows.length === 1) {
const row = await prisma.machineCycle.create({ data: rows[0] });
return NextResponse.json({ ok: true, id: row.id, ts: row.ts });
}
const result = await prisma.machineCycle.createMany({ data: rows });
return NextResponse.json({ ok: true, count: result.count });
}

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getMachineAuth } from "@/lib/machineAuthCache";
import { z } from "zod";
const normalizeType = (t: any) =>
@@ -83,10 +84,7 @@ export async function POST(req: Request) {
return NextResponse.json({ ok: false, error: "Invalid machine id" }, { status: 400 });
}
const machine = await prisma.machine.findFirst({
where: { id: String(machineId), apiKey },
select: { id: true, orgId: true },
});
const machine = await getMachineAuth(String(machineId), apiKey);
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
const orgSettings = await prisma.orgSettings.findUnique({
where: { orgId: machine.orgId },

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getMachineAuth } from "@/lib/machineAuthCache";
import { normalizeHeartbeatV1 } from "@/lib/contracts/v1";
function getClientIp(req: Request) {
@@ -59,10 +60,7 @@ export async function POST(req: Request) {
tsDeviceDate = new Date(body.tsDevice);
// 4) Authorize machineId + apiKey
const machine = await prisma.machine.findFirst({
where: { id: machineId, apiKey },
select: { id: true, orgId: true },
});
const machine = await getMachineAuth(machineId, apiKey);
if (!machine) {
await prisma.ingestLog.create({
@@ -117,23 +115,6 @@ export async function POST(req: Request) {
},
});
// 6) Ingest log success
await prisma.ingestLog.create({
data: {
orgId,
machineId: machine.id,
endpoint,
ok: true,
status: 200,
schemaVersion,
seq,
tsDevice: tsDeviceDate,
body: rawBody,
ip,
userAgent,
},
});
return NextResponse.json({
ok: true,
id: hb.id,

View File

@@ -1,6 +1,7 @@
// mis-control-tower/app/api/ingest/kpi/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getMachineAuth } from "@/lib/machineAuthCache";
import { normalizeSnapshotV1 } from "@/lib/contracts/v1";
function getClientIp(req: Request) {
@@ -75,10 +76,7 @@ export async function POST(req: Request) {
tsDeviceDate = new Date(body.tsDevice);
// Auth: machineId + apiKey must match
const machine = await prisma.machine.findFirst({
where: { id: machineId, apiKey },
select: { id: true, orgId: true },
});
const machine = await getMachineAuth(machineId, apiKey);
if (!machine) {
await prisma.ingestLog.create({
@@ -165,22 +163,6 @@ export async function POST(req: Request) {
},
});
await prisma.ingestLog.create({
data: {
orgId,
machineId: machine.id,
endpoint,
ok: true,
status: 200,
schemaVersion,
seq,
tsDevice: tsDeviceDate,
body: rawBody,
ip,
userAgent,
},
});
return NextResponse.json({
ok: true,
id: row.id,

View File

@@ -275,7 +275,7 @@ const events = normalized
// ---- cycles window ----
const url = new URL(_req.url);
const windowSec = Number(url.searchParams.get("windowSec") ?? "10800"); // default 3h
const windowSec = Number(url.searchParams.get("windowSec") ?? "3600"); // default 1h
const latestKpi = machine.kpiSnapshots[0] ?? null;
@@ -297,7 +297,7 @@ const estCycleSec = Math.max(1, Number(effectiveCycleTime ?? 14));
const needed = Math.ceil(windowSec / estCycleSec) + 50;
// Safety cap to avoid crazy payloads
const takeCycles = Math.min(5000, Math.max(200, needed));
const takeCycles = Math.min(1000, Math.max(200, needed));
const rawCycles = await prisma.machineCycle.findMany({
where: { orgId: session.orgId, machineId },