Macrostop and timeline segmentation
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
@@ -76,6 +76,11 @@ type MachineDetail = {
|
|||||||
latestKpi: Kpi | null;
|
latestKpi: Kpi | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Thresholds = {
|
||||||
|
stoppageMultiplier: number;
|
||||||
|
macroStoppageMultiplier: number;
|
||||||
|
};
|
||||||
|
|
||||||
type TimelineState = "normal" | "slow" | "microstop" | "macrostop";
|
type TimelineState = "normal" | "slow" | "microstop" | "macrostop";
|
||||||
|
|
||||||
type TimelineSeg = {
|
type TimelineSeg = {
|
||||||
@@ -85,32 +90,148 @@ type TimelineSeg = {
|
|||||||
state: TimelineState;
|
state: TimelineState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UploadState = {
|
||||||
|
status: "idle" | "parsing" | "uploading" | "success" | "error";
|
||||||
|
message?: string;
|
||||||
|
count?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkOrderUpload = {
|
||||||
|
workOrderId: string;
|
||||||
|
sku?: string;
|
||||||
|
targetQty?: number;
|
||||||
|
cycleTime?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const TOL = 0.10;
|
const TOL = 0.10;
|
||||||
|
const DEFAULT_MICRO_MULT = 1.5;
|
||||||
|
const DEFAULT_MACRO_MULT = 5;
|
||||||
|
const NORMAL_TOL_SEC = 0.1;
|
||||||
|
|
||||||
function classifyGap(dtSec: number, idealSec: number): TimelineState {
|
|
||||||
const SLOW_X = 1.5;
|
|
||||||
const STOP_X = 3.0;
|
|
||||||
const MACRO_X = 10.0;
|
|
||||||
|
|
||||||
if (dtSec <= idealSec * SLOW_X) return "normal";
|
function resolveMultipliers(thresholds?: Thresholds | null) {
|
||||||
if (dtSec <= idealSec * STOP_X) return "slow";
|
const micro = Number(thresholds?.stoppageMultiplier ?? DEFAULT_MICRO_MULT);
|
||||||
if (dtSec <= idealSec * MACRO_X) return "microstop";
|
const macro = Math.max(
|
||||||
|
micro,
|
||||||
|
Number(thresholds?.macroStoppageMultiplier ?? DEFAULT_MACRO_MULT)
|
||||||
|
);
|
||||||
|
return { micro, macro };
|
||||||
|
}
|
||||||
|
|
||||||
|
function classifyCycleDuration(
|
||||||
|
actualSec: number,
|
||||||
|
idealSec: number,
|
||||||
|
thresholds?: Thresholds | null
|
||||||
|
): TimelineState {
|
||||||
|
const { micro, macro } = resolveMultipliers(thresholds);
|
||||||
|
|
||||||
|
if (actualSec < idealSec + NORMAL_TOL_SEC) return "normal";
|
||||||
|
if (actualSec < idealSec * micro) return "slow";
|
||||||
|
if (actualSec < idealSec * macro) return "microstop";
|
||||||
return "macrostop";
|
return "macrostop";
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeAdjacent(segs: TimelineSeg[]): TimelineSeg[] {
|
|
||||||
if (!segs.length) return [];
|
const WORK_ORDER_KEYS = {
|
||||||
const out: TimelineSeg[] = [segs[0]];
|
id: new Set(["workorderid", "workorder", "orderid", "woid", "work_order_id", "otid"]),
|
||||||
for (let i = 1; i < segs.length; i++) {
|
sku: new Set(["sku"]),
|
||||||
const prev = out[out.length - 1];
|
cycle: new Set([
|
||||||
const cur = segs[i];
|
"theoreticalcycletimeseconds",
|
||||||
if (cur.state === prev.state && cur.start <= prev.end + 1) {
|
"theoreticalcycletime",
|
||||||
prev.end = Math.max(prev.end, cur.end);
|
"cycletime",
|
||||||
prev.durationSec = (prev.end - prev.start) / 1000;
|
"cycle_time",
|
||||||
} else {
|
"theoretical_cycle_time",
|
||||||
out.push(cur);
|
]),
|
||||||
|
target: new Set(["targetquantity", "targetqty", "target", "target_qty"]),
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeKey(value: string) {
|
||||||
|
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCsvText(text: string) {
|
||||||
|
const rows: string[][] = [];
|
||||||
|
let row: string[] = [];
|
||||||
|
let field = "";
|
||||||
|
let inQuotes = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; i += 1) {
|
||||||
|
const ch = text[i];
|
||||||
|
|
||||||
|
if (ch === "\"") {
|
||||||
|
if (inQuotes && text[i + 1] === "\"") {
|
||||||
|
field += "\"";
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ch === "," && !inQuotes) {
|
||||||
|
row.push(field);
|
||||||
|
field = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ch === "\n" || ch === "\r") && !inQuotes) {
|
||||||
|
if (ch === "\r" && text[i + 1] === "\n") i += 1;
|
||||||
|
row.push(field);
|
||||||
|
field = "";
|
||||||
|
if (row.some((cell) => cell.trim().length > 0)) {
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
row = [];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
field += ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
row.push(field);
|
||||||
|
if (row.some((cell) => cell.trim().length > 0)) {
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows.length) return [];
|
||||||
|
|
||||||
|
const headers = rows.shift()!.map((h) => h.trim());
|
||||||
|
return rows.map((cols) => {
|
||||||
|
const obj: Record<string, string> = {};
|
||||||
|
headers.forEach((header, idx) => {
|
||||||
|
obj[header] = (cols[idx] ?? "").trim();
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickRowValue(row: Record<string, any>, keys: Set<string>) {
|
||||||
|
for (const [key, value] of Object.entries(row)) {
|
||||||
|
if (keys.has(normalizeKey(key))) return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowsToWorkOrders(rows: Array<Record<string, any>>): WorkOrderUpload[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: WorkOrderUpload[] = [];
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const rawId = pickRowValue(row, WORK_ORDER_KEYS.id);
|
||||||
|
const workOrderId = String(rawId ?? "").trim();
|
||||||
|
if (!workOrderId || seen.has(workOrderId)) return;
|
||||||
|
seen.add(workOrderId);
|
||||||
|
|
||||||
|
const sku = String(pickRowValue(row, WORK_ORDER_KEYS.sku) ?? "").trim();
|
||||||
|
const targetRaw = pickRowValue(row, WORK_ORDER_KEYS.target);
|
||||||
|
const cycleRaw = pickRowValue(row, WORK_ORDER_KEYS.cycle);
|
||||||
|
|
||||||
|
const targetQty = Number.isFinite(Number(targetRaw)) ? Math.trunc(Number(targetRaw)) : undefined;
|
||||||
|
const cycleTime = Number.isFinite(Number(cycleRaw)) ? Number(cycleRaw) : undefined;
|
||||||
|
|
||||||
|
out.push({ workOrderId, sku: sku || undefined, targetQty, cycleTime });
|
||||||
|
});
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +245,10 @@ export default function MachineDetailClient() {
|
|||||||
const [events, setEvents] = useState<EventRow[]>([]);
|
const [events, setEvents] = useState<EventRow[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [cycles, setCycles] = useState<CycleRow[]>([]);
|
const [cycles, setCycles] = useState<CycleRow[]>([]);
|
||||||
|
const [thresholds, setThresholds] = useState<Thresholds | null>(null);
|
||||||
const [open, setOpen] = useState<null | "events" | "deviation" | "impact">(null);
|
const [open, setOpen] = useState<null | "events" | "deviation" | "impact">(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [uploadState, setUploadState] = useState<UploadState>({ status: "idle" });
|
||||||
|
|
||||||
const BUCKET = {
|
const BUCKET = {
|
||||||
normal: {
|
normal: {
|
||||||
@@ -183,6 +307,7 @@ export default function MachineDetailClient() {
|
|||||||
setMachine(json.machine ?? null);
|
setMachine(json.machine ?? null);
|
||||||
setEvents(json.events ?? []);
|
setEvents(json.events ?? []);
|
||||||
setCycles(json.cycles ?? []);
|
setCycles(json.cycles ?? []);
|
||||||
|
setThresholds(json.thresholds ?? null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -200,6 +325,101 @@ export default function MachineDetailClient() {
|
|||||||
};
|
};
|
||||||
}, [machineId, t]);
|
}, [machineId, t]);
|
||||||
|
|
||||||
|
async function parseWorkOrdersFile(file: File) {
|
||||||
|
const name = file.name.toLowerCase();
|
||||||
|
if (name.endsWith(".csv")) {
|
||||||
|
const text = await file.text();
|
||||||
|
return rowsToWorkOrders(parseCsvText(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.endsWith(".xls") || name.endsWith(".xlsx")) {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const xlsx = await import("xlsx");
|
||||||
|
const workbook = xlsx.read(buffer, { type: "array" });
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||||
|
if (!sheet) return [];
|
||||||
|
const rows = xlsx.utils.sheet_to_json(sheet, { defval: "" });
|
||||||
|
return rowsToWorkOrders(rows as Array<Record<string, any>>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWorkOrderUpload(event: any) {
|
||||||
|
const file = event?.target?.files?.[0] as File | undefined;
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!machineId) {
|
||||||
|
setUploadState({ status: "error", message: t("machine.detail.workOrders.uploadError") });
|
||||||
|
event.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadState({ status: "parsing", message: t("machine.detail.workOrders.uploadParsing") });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workOrders = await parseWorkOrdersFile(file);
|
||||||
|
if (!workOrders) {
|
||||||
|
setUploadState({ status: "error", message: t("machine.detail.workOrders.uploadInvalid") });
|
||||||
|
event.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workOrders.length) {
|
||||||
|
setUploadState({ status: "error", message: t("machine.detail.workOrders.uploadInvalid") });
|
||||||
|
event.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadState({ status: "uploading", message: t("machine.detail.workOrders.uploading") });
|
||||||
|
|
||||||
|
const res = await fetch("/api/work-orders", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ machineId, workOrders }),
|
||||||
|
});
|
||||||
|
const json = await res.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (!res.ok || json?.ok === false) {
|
||||||
|
if (res.status === 401 || res.status === 403) {
|
||||||
|
setUploadState({ status: "error", message: t("machine.detail.workOrders.uploadUnauthorized") });
|
||||||
|
} else {
|
||||||
|
setUploadState({
|
||||||
|
status: "error",
|
||||||
|
message: json?.error ?? t("machine.detail.workOrders.uploadError"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
event.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadState({
|
||||||
|
status: "success",
|
||||||
|
message: t("machine.detail.workOrders.uploadSuccess", { count: workOrders.length }),
|
||||||
|
count: workOrders.length,
|
||||||
|
});
|
||||||
|
event.target.value = "";
|
||||||
|
} catch {
|
||||||
|
setUploadState({ status: "error", message: t("machine.detail.workOrders.uploadError") });
|
||||||
|
event.target.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadButtonLabel =
|
||||||
|
uploadState.status === "parsing"
|
||||||
|
? t("machine.detail.workOrders.uploadParsing")
|
||||||
|
: uploadState.status === "uploading"
|
||||||
|
? t("machine.detail.workOrders.uploading")
|
||||||
|
: t("machine.detail.workOrders.upload");
|
||||||
|
const uploadStatusClass =
|
||||||
|
uploadState.status === "success"
|
||||||
|
? "bg-emerald-500/15 text-emerald-300 border-emerald-500/20"
|
||||||
|
: uploadState.status === "error"
|
||||||
|
? "bg-red-500/15 text-red-300 border-red-500/20"
|
||||||
|
: "bg-white/10 text-zinc-200 border-white/10";
|
||||||
|
const isUploading = uploadState.status === "parsing" || uploadState.status === "uploading";
|
||||||
|
|
||||||
function fmtPct(v?: number | null) {
|
function fmtPct(v?: number | null) {
|
||||||
if (v === null || v === undefined || Number.isNaN(v)) return t("common.na");
|
if (v === null || v === undefined || Number.isNaN(v)) return t("common.na");
|
||||||
return `${v.toFixed(1)}%`;
|
return `${v.toFixed(1)}%`;
|
||||||
@@ -474,6 +694,7 @@ export default function MachineDetailClient() {
|
|||||||
|
|
||||||
const cycleDerived = useMemo(() => {
|
const cycleDerived = useMemo(() => {
|
||||||
const rows = cycles ?? [];
|
const rows = cycles ?? [];
|
||||||
|
const { micro, macro } = resolveMultipliers(thresholds);
|
||||||
|
|
||||||
const mapped: CycleDerivedRow[] = rows.map((cycle) => {
|
const mapped: CycleDerivedRow[] = rows.map((cycle) => {
|
||||||
const ideal = cycle.ideal ?? null;
|
const ideal = cycle.ideal ?? null;
|
||||||
@@ -482,10 +703,7 @@ export default function MachineDetailClient() {
|
|||||||
|
|
||||||
let bucket: CycleDerivedRow["bucket"] = "unknown";
|
let bucket: CycleDerivedRow["bucket"] = "unknown";
|
||||||
if (ideal != null && actual != null) {
|
if (ideal != null && actual != null) {
|
||||||
if (actual <= ideal * (1 + TOL)) bucket = "normal";
|
bucket = classifyCycleDuration(actual, ideal, thresholds);
|
||||||
else if (extra != null && extra <= 1) bucket = "slow";
|
|
||||||
else if (extra != null && extra <= 10) bucket = "microstop";
|
|
||||||
else bucket = "macrostop";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...cycle, ideal, actual, extra, bucket };
|
return { ...cycle, ideal, actual, extra, bucket };
|
||||||
@@ -505,7 +723,7 @@ export default function MachineDetailClient() {
|
|||||||
const avgDeltaPct = deltas.length ? deltas.reduce((a, b) => a + b, 0) / deltas.length : null;
|
const avgDeltaPct = deltas.length ? deltas.reduce((a, b) => a + b, 0) / deltas.length : null;
|
||||||
|
|
||||||
return { mapped, counts, avgDeltaPct };
|
return { mapped, counts, avgDeltaPct };
|
||||||
}, [cycles]);
|
}, [cycles, thresholds]);
|
||||||
|
|
||||||
const deviationSeries = useMemo(() => {
|
const deviationSeries = useMemo(() => {
|
||||||
const last = cycleDerived.mapped.slice(-100);
|
const last = cycleDerived.mapped.slice(-100);
|
||||||
@@ -557,7 +775,7 @@ export default function MachineDetailClient() {
|
|||||||
|
|
||||||
const timeline = useMemo(() => {
|
const timeline = useMemo(() => {
|
||||||
const rows = cycles ?? [];
|
const rows = cycles ?? [];
|
||||||
if (rows.length < 2) {
|
if (rows.length < 1) {
|
||||||
return {
|
return {
|
||||||
windowSec: 10800,
|
windowSec: 10800,
|
||||||
segments: [] as TimelineSeg[],
|
segments: [] as TimelineSeg[],
|
||||||
@@ -570,27 +788,24 @@ export default function MachineDetailClient() {
|
|||||||
const end = rows[rows.length - 1].t;
|
const end = rows[rows.length - 1].t;
|
||||||
const start = end - windowSec * 1000;
|
const start = end - windowSec * 1000;
|
||||||
|
|
||||||
const idxFirst = Math.max(
|
|
||||||
0,
|
|
||||||
rows.findIndex((row) => row.t >= start) - 1
|
|
||||||
);
|
|
||||||
const sliced = rows.slice(idxFirst);
|
|
||||||
|
|
||||||
const segs: TimelineSeg[] = [];
|
const segs: TimelineSeg[] = [];
|
||||||
|
|
||||||
for (let i = 1; i < sliced.length; i++) {
|
for (const cycle of rows) {
|
||||||
const prev = sliced[i - 1];
|
const ideal = (cycle.ideal ?? cycleTarget ?? 0) as number;
|
||||||
const cur = sliced[i];
|
const actual = cycle.actual ?? 0;
|
||||||
|
if (!ideal || ideal <= 0 || !actual || actual <= 0) continue;
|
||||||
|
|
||||||
const segStart = Math.max(prev.t, start);
|
const cycleEnd = cycle.t;
|
||||||
const segEnd = Math.min(cur.t, end);
|
const cycleStart = cycleEnd - actual * 1000;
|
||||||
|
if (cycleEnd <= start || cycleStart >= end) continue;
|
||||||
|
|
||||||
|
const segStart = Math.max(cycleStart, start);
|
||||||
|
const segEnd = Math.min(cycleEnd, end);
|
||||||
if (segEnd <= segStart) continue;
|
if (segEnd <= segStart) continue;
|
||||||
|
|
||||||
const dtSec = (cur.t - prev.t) / 1000;
|
const state = classifyCycleDuration(actual, ideal, thresholds);
|
||||||
const ideal = (cur.ideal ?? prev.ideal ?? cycleTarget ?? 0) as number;
|
|
||||||
if (!ideal || ideal <= 0) continue;
|
|
||||||
|
|
||||||
const state = classifyGap(dtSec, ideal);
|
|
||||||
|
|
||||||
segs.push({
|
segs.push({
|
||||||
start: segStart,
|
start: segStart,
|
||||||
@@ -600,9 +815,8 @@ export default function MachineDetailClient() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const segments = mergeAdjacent(segs);
|
return { windowSec, segments: segs, start, end };
|
||||||
return { windowSec, segments, start, end };
|
}, [cycles, cycleTarget, thresholds]);
|
||||||
}, [cycles, cycleTarget]);
|
|
||||||
|
|
||||||
const cycleTargetLabel = cycleTarget ? `${cycleTarget}s` : t("common.na");
|
const cycleTargetLabel = cycleTarget ? `${cycleTarget}s` : t("common.na");
|
||||||
const workOrderLabel = kpi?.workOrderId ?? t("common.na");
|
const workOrderLabel = kpi?.workOrderId ?? t("common.na");
|
||||||
@@ -625,13 +839,38 @@ export default function MachineDetailClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="flex shrink-0 flex-col items-end gap-2">
|
||||||
<Link
|
<div className="flex items-center gap-2">
|
||||||
href="/machines"
|
<input
|
||||||
className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-white hover:bg-white/10"
|
ref={fileInputRef}
|
||||||
>
|
type="file"
|
||||||
{t("machine.detail.back")}
|
accept=".csv,.xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
</Link>
|
className="hidden"
|
||||||
|
onChange={handleWorkOrderUpload}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={isUploading}
|
||||||
|
className="rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-2 text-sm text-emerald-100 transition hover:bg-emerald-500/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{uploadButtonLabel}
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
href="/machines"
|
||||||
|
className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{t("machine.detail.back")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="text-right text-[11px] text-zinc-500">
|
||||||
|
{t("machine.detail.workOrders.uploadHint")}
|
||||||
|
</div>
|
||||||
|
{uploadState.status !== "idle" && uploadState.message && (
|
||||||
|
<div className={`rounded-full border px-3 py-1 text-xs ${uploadStatusClass}`}>
|
||||||
|
{uploadState.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -297,8 +297,21 @@ export default function MachinesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 text-sm text-zinc-400">{t("machines.status")}</div>
|
<div className="mt-4 text-sm text-zinc-400">{t("machines.status")}</div>
|
||||||
<div className="text-xl font-semibold text-white">
|
<div className="mt-1 flex items-center gap-2 text-sm font-semibold text-white">
|
||||||
{offline ? t("machines.status.noHeartbeat") : (hb?.message ?? t("machines.status.ok"))}
|
{offline ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-flex h-2.5 w-2.5 rounded-full bg-zinc-500" aria-hidden="true" />
|
||||||
|
<span>{t("machines.status.noHeartbeat")}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="relative flex h-2.5 w-2.5" aria-hidden="true">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-400" />
|
||||||
|
</span>
|
||||||
|
<span>{t("machines.status.ok")}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -310,4 +323,3 @@ export default function MachinesPage() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ type MachineRow = {
|
|||||||
latestKpi?: Kpi | null;
|
latestKpi?: Kpi | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Thresholds = {
|
||||||
|
stoppageMultiplier: number;
|
||||||
|
macroStoppageMultiplier: number;
|
||||||
|
};
|
||||||
|
|
||||||
type EventRow = {
|
type EventRow = {
|
||||||
id: string;
|
id: string;
|
||||||
ts: string;
|
ts: string;
|
||||||
@@ -60,7 +65,17 @@ type CycleRow = {
|
|||||||
const OFFLINE_MS = 30000;
|
const OFFLINE_MS = 30000;
|
||||||
const EVENT_WINDOW_SEC = 1800;
|
const EVENT_WINDOW_SEC = 1800;
|
||||||
const MAX_EVENT_MACHINES = 6;
|
const MAX_EVENT_MACHINES = 6;
|
||||||
const TOL = 0.10;
|
const DEFAULT_MICRO_MULT = 1.5;
|
||||||
|
const DEFAULT_MACRO_MULT = 5;
|
||||||
|
|
||||||
|
function resolveMultipliers(thresholds?: Thresholds | null) {
|
||||||
|
const micro = Number(thresholds?.stoppageMultiplier ?? DEFAULT_MICRO_MULT);
|
||||||
|
const macro = Math.max(
|
||||||
|
micro,
|
||||||
|
Number(thresholds?.macroStoppageMultiplier ?? DEFAULT_MACRO_MULT)
|
||||||
|
);
|
||||||
|
return { micro, macro };
|
||||||
|
}
|
||||||
|
|
||||||
function secondsAgo(ts: string | undefined, locale: string, fallback: string) {
|
function secondsAgo(ts: string | undefined, locale: string, fallback: string) {
|
||||||
if (!ts) return fallback;
|
if (!ts) return fallback;
|
||||||
@@ -106,16 +121,17 @@ function sourceClass(src: EventRow["source"]) {
|
|||||||
: "bg-emerald-500/15 text-emerald-300";
|
: "bg-emerald-500/15 text-emerald-300";
|
||||||
}
|
}
|
||||||
|
|
||||||
function classifyDerivedEvent(c: CycleRow) {
|
function classifyDerivedEvent(c: CycleRow, thresholds?: Thresholds | null) {
|
||||||
if (c.ideal == null || c.ideal <= 0 || c.actual <= 0) return null;
|
if (c.ideal == null || c.ideal <= 0 || c.actual <= 0) return null;
|
||||||
if (c.actual <= c.ideal * (1 + TOL)) return null;
|
if (c.actual <= c.ideal) return null;
|
||||||
|
const { micro, macro } = resolveMultipliers(thresholds);
|
||||||
const extra = c.actual - c.ideal;
|
const extra = c.actual - c.ideal;
|
||||||
let eventType = "slow-cycle";
|
let eventType = "slow-cycle";
|
||||||
let severity = "warning";
|
let severity = "warning";
|
||||||
if (extra <= 1) {
|
if (c.actual < c.ideal * micro) {
|
||||||
eventType = "slow-cycle";
|
eventType = "slow-cycle";
|
||||||
severity = "info";
|
severity = "warning";
|
||||||
} else if (extra <= 10) {
|
} else if (c.actual < c.ideal * macro) {
|
||||||
eventType = "microstop";
|
eventType = "microstop";
|
||||||
severity = "warning";
|
severity = "warning";
|
||||||
} else {
|
} else {
|
||||||
@@ -216,7 +232,7 @@ export default function OverviewPage() {
|
|||||||
|
|
||||||
const cycles: CycleRow[] = Array.isArray(payload?.cycles) ? payload.cycles : [];
|
const cycles: CycleRow[] = Array.isArray(payload?.cycles) ? payload.cycles : [];
|
||||||
for (const c of cycles.slice(-120)) {
|
for (const c of cycles.slice(-120)) {
|
||||||
const derived = classifyDerivedEvent(c);
|
const derived = classifyDerivedEvent(c, payload?.thresholds);
|
||||||
if (!derived) continue;
|
if (!derived) continue;
|
||||||
combined.push({
|
combined.push({
|
||||||
id: `derived-${machine.id}-${c.t}`,
|
id: `derived-${machine.id}-${c.t}`,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type SettingsPayload = {
|
|||||||
};
|
};
|
||||||
thresholds: {
|
thresholds: {
|
||||||
stoppageMultiplier: number;
|
stoppageMultiplier: number;
|
||||||
|
macroStoppageMultiplier: number;
|
||||||
oeeAlertThresholdPct: number;
|
oeeAlertThresholdPct: number;
|
||||||
performanceThresholdPct: number;
|
performanceThresholdPct: number;
|
||||||
qualitySpikeDeltaPct: number;
|
qualitySpikeDeltaPct: number;
|
||||||
@@ -82,6 +83,7 @@ const DEFAULT_SETTINGS: SettingsPayload = {
|
|||||||
thresholds: {
|
thresholds: {
|
||||||
stoppageMultiplier: 1.5,
|
stoppageMultiplier: 1.5,
|
||||||
oeeAlertThresholdPct: 90,
|
oeeAlertThresholdPct: 90,
|
||||||
|
macroStoppageMultiplier: 5,
|
||||||
performanceThresholdPct: 85,
|
performanceThresholdPct: 85,
|
||||||
qualitySpikeDeltaPct: 5,
|
qualitySpikeDeltaPct: 5,
|
||||||
},
|
},
|
||||||
@@ -151,6 +153,9 @@ function normalizeSettings(raw: any, fallbackName: (index: number) => string): S
|
|||||||
stoppageMultiplier: Number(
|
stoppageMultiplier: Number(
|
||||||
raw.thresholds?.stoppageMultiplier ?? DEFAULT_SETTINGS.thresholds.stoppageMultiplier
|
raw.thresholds?.stoppageMultiplier ?? DEFAULT_SETTINGS.thresholds.stoppageMultiplier
|
||||||
),
|
),
|
||||||
|
macroStoppageMultiplier: Number(
|
||||||
|
raw.thresholds?.macroStoppageMultiplier ?? DEFAULT_SETTINGS.thresholds.macroStoppageMultiplier
|
||||||
|
),
|
||||||
oeeAlertThresholdPct: Number(
|
oeeAlertThresholdPct: Number(
|
||||||
raw.thresholds?.oeeAlertThresholdPct ?? DEFAULT_SETTINGS.thresholds.oeeAlertThresholdPct
|
raw.thresholds?.oeeAlertThresholdPct ?? DEFAULT_SETTINGS.thresholds.oeeAlertThresholdPct
|
||||||
),
|
),
|
||||||
@@ -351,6 +356,7 @@ export default function SettingsPage() {
|
|||||||
(
|
(
|
||||||
key:
|
key:
|
||||||
| "stoppageMultiplier"
|
| "stoppageMultiplier"
|
||||||
|
| "macroStoppageMultiplier"
|
||||||
| "oeeAlertThresholdPct"
|
| "oeeAlertThresholdPct"
|
||||||
| "performanceThresholdPct"
|
| "performanceThresholdPct"
|
||||||
| "qualitySpikeDeltaPct",
|
| "qualitySpikeDeltaPct",
|
||||||
@@ -651,6 +657,20 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="rounded-xl border border-white/10 bg-black/20 p-3 text-xs text-zinc-400">
|
<label className="rounded-xl border border-white/10 bg-black/20 p-3 text-xs text-zinc-400">
|
||||||
|
{t("settings.thresholds.macroStoppage")}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1.1}
|
||||||
|
max={20}
|
||||||
|
step={0.1}
|
||||||
|
value={draft.thresholds.macroStoppageMultiplier}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateThreshold("macroStoppageMultiplier", Number(event.target.value))
|
||||||
|
}
|
||||||
|
className="mt-2 w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="rounded-xl border border-white/10 bg-black/20 p-3 text-xs text-zinc-400">
|
||||||
{t("settings.thresholds.performance")} (%)
|
{t("settings.thresholds.performance")} (%)
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
function unwrapEnvelope(raw: any) {
|
function unwrapEnvelope(raw: any) {
|
||||||
if (!raw || typeof raw !== "object") return raw;
|
if (!raw || typeof raw !== "object") return raw;
|
||||||
@@ -25,6 +26,39 @@ function unwrapEnvelope(raw: any) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const numberFromAny = z.preprocess((value) => {
|
||||||
|
if (typeof value === "number") return value;
|
||||||
|
if (typeof value === "string" && value.trim() !== "") return Number(value);
|
||||||
|
return value;
|
||||||
|
}, z.number().finite());
|
||||||
|
|
||||||
|
const intFromAny = z.preprocess((value) => {
|
||||||
|
if (typeof value === "number") return Math.trunc(value);
|
||||||
|
if (typeof value === "string" && value.trim() !== "") return Math.trunc(Number(value));
|
||||||
|
return value;
|
||||||
|
}, z.number().int().finite());
|
||||||
|
|
||||||
|
const cyclePayloadSchema = 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(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const apiKey = req.headers.get("x-api-key");
|
const apiKey = req.headers.get("x-api-key");
|
||||||
if (!apiKey) return NextResponse.json({ ok: false, error: "Missing api key" }, { status: 401 });
|
if (!apiKey) return NextResponse.json({ ok: false, error: "Missing api key" }, { status: 401 });
|
||||||
@@ -32,24 +66,26 @@ export async function POST(req: Request) {
|
|||||||
let body = await req.json().catch(() => null);
|
let body = await req.json().catch(() => null);
|
||||||
body = unwrapEnvelope(body);
|
body = unwrapEnvelope(body);
|
||||||
|
|
||||||
if (!body?.machineId || !body?.cycle) {
|
const parsed = cyclePayloadSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
|
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const machine = await prisma.machine.findFirst({
|
const machine = await prisma.machine.findFirst({
|
||||||
where: { id: String(body.machineId), apiKey },
|
where: { id: parsed.data.machineId, apiKey },
|
||||||
select: { id: true, orgId: true },
|
select: { id: true, orgId: true },
|
||||||
});
|
});
|
||||||
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const c = body.cycle;
|
const c = parsed.data.cycle;
|
||||||
|
const raw = body as any;
|
||||||
|
|
||||||
const tsMs =
|
const tsMs =
|
||||||
(typeof c.timestamp === "number" && c.timestamp) ||
|
(typeof c.timestamp === "number" && c.timestamp) ||
|
||||||
(typeof c.ts === "number" && c.ts) ||
|
(typeof c.ts === "number" && c.ts) ||
|
||||||
(typeof c.event_timestamp === "number" && c.event_timestamp) ||
|
(typeof c.event_timestamp === "number" && c.event_timestamp) ||
|
||||||
(typeof body.tsMs === "number" && body.tsMs) ||
|
(typeof raw?.tsMs === "number" && raw.tsMs) ||
|
||||||
(typeof body.tsDevice === "number" && body.tsDevice) ||
|
(typeof raw?.tsDevice === "number" && raw.tsDevice) ||
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
const ts = tsMs ? new Date(tsMs) : new Date();
|
const ts = tsMs ? new Date(tsMs) : new Date();
|
||||||
@@ -60,8 +96,8 @@ export async function POST(req: Request) {
|
|||||||
machineId: machine.id,
|
machineId: machine.id,
|
||||||
ts,
|
ts,
|
||||||
cycleCount: typeof c.cycle_count === "number" ? c.cycle_count : null,
|
cycleCount: typeof c.cycle_count === "number" ? c.cycle_count : null,
|
||||||
actualCycleTime: Number(c.actual_cycle_time),
|
actualCycleTime: c.actual_cycle_time,
|
||||||
theoreticalCycleTime: c.theoretical_cycle_time != null ? Number(c.theoretical_cycle_time) : null,
|
theoreticalCycleTime: typeof c.theoretical_cycle_time === "number" ? c.theoretical_cycle_time : null,
|
||||||
workOrderId: c.work_order_id ? String(c.work_order_id) : null,
|
workOrderId: c.work_order_id ? String(c.work_order_id) : null,
|
||||||
sku: c.sku ? String(c.sku) : null,
|
sku: c.sku ? String(c.sku) : null,
|
||||||
cavities: typeof c.cavities === "number" ? c.cavities : null,
|
cavities: typeof c.cavities === "number" ? c.cavities : null,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const normalizeType = (t: any) =>
|
const normalizeType = (t: any) =>
|
||||||
String(t ?? "")
|
String(t ?? "")
|
||||||
@@ -33,9 +34,19 @@ const ALLOWED_TYPES = new Set([
|
|||||||
"predictive-oee-decline",
|
"predictive-oee-decline",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// thresholds for stop classification (tune later / move to machine config)
|
const machineIdSchema = z.string().uuid();
|
||||||
const MICROSTOP_SEC = 60;
|
const MAX_EVENTS = 100;
|
||||||
const MACROSTOP_SEC = 300;
|
|
||||||
|
//when no cycle time is configed
|
||||||
|
const DEFAULT_MACROSTOP_SEC = 300;
|
||||||
|
|
||||||
|
|
||||||
|
function clampText(value: unknown, maxLen: number) {
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
const text = String(value).trim().replace(/[\u0000-\u001f\u007f]/g, "");
|
||||||
|
if (!text) return null;
|
||||||
|
return text.length > maxLen ? text.slice(0, maxLen) : text;
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const apiKey = req.headers.get("x-api-key");
|
const apiKey = req.headers.get("x-api-key");
|
||||||
@@ -68,14 +79,32 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!machineIdSchema.safeParse(String(machineId)).success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid machine id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const machine = await prisma.machine.findFirst({
|
const machine = await prisma.machine.findFirst({
|
||||||
where: { id: String(machineId), apiKey },
|
where: { id: String(machineId), apiKey },
|
||||||
select: { id: true, orgId: true },
|
select: { id: true, orgId: true },
|
||||||
});
|
});
|
||||||
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
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 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultMicroMultiplier = Number(orgSettings?.stoppageMultiplier ?? 1.5);
|
||||||
|
const defaultMacroMultiplier = Math.max(
|
||||||
|
defaultMicroMultiplier,
|
||||||
|
Number(orgSettings?.macroStoppageMultiplier ?? 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// ✅ normalize to array no matter what
|
// ✅ normalize to array no matter what
|
||||||
const events = Array.isArray(rawEvent) ? rawEvent : [rawEvent];
|
const events = Array.isArray(rawEvent) ? rawEvent : [rawEvent];
|
||||||
|
if (events.length > MAX_EVENTS) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Too many events" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const created: { id: string; ts: Date; eventType: string }[] = [];
|
const created: { id: string; ts: Date; eventType: string }[] = [];
|
||||||
const skipped: any[] = [];
|
const skipped: any[] = [];
|
||||||
@@ -112,7 +141,29 @@ export async function POST(req: Request) {
|
|||||||
null;
|
null;
|
||||||
|
|
||||||
if (stopSec != null) {
|
if (stopSec != null) {
|
||||||
finalType = stopSec >= MACROSTOP_SEC ? "macrostop" : "microstop";
|
const theoretical =
|
||||||
|
Number(
|
||||||
|
(ev as any)?.data?.theoretical_cycle_time ??
|
||||||
|
(ev as any)?.data?.theoreticalCycleTime ??
|
||||||
|
0
|
||||||
|
) || 0;
|
||||||
|
|
||||||
|
const microMultiplier = Number(
|
||||||
|
(ev as any)?.data?.micro_threshold_multiplier ??
|
||||||
|
(ev as any)?.data?.threshold_multiplier ??
|
||||||
|
defaultMicroMultiplier
|
||||||
|
);
|
||||||
|
const macroMultiplier = Math.max(
|
||||||
|
microMultiplier,
|
||||||
|
Number((ev as any)?.data?.macro_threshold_multiplier ?? defaultMacroMultiplier)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (theoretical > 0) {
|
||||||
|
const macroThresholdSec = theoretical * macroMultiplier;
|
||||||
|
finalType = stopSec >= macroThresholdSec ? "macrostop" : "microstop";
|
||||||
|
} else {
|
||||||
|
finalType = stopSec >= DEFAULT_MACROSTOP_SEC ? "macrostop" : "microstop";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// missing duration -> conservative
|
// missing duration -> conservative
|
||||||
finalType = "microstop";
|
finalType = "microstop";
|
||||||
@@ -125,13 +176,13 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const title =
|
const title =
|
||||||
String((ev as any).title ?? "").trim() ||
|
clampText((ev as any).title, 160) ||
|
||||||
(finalType === "slow-cycle" ? "Slow Cycle Detected" :
|
(finalType === "slow-cycle" ? "Slow Cycle Detected" :
|
||||||
finalType === "macrostop" ? "Macrostop Detected" :
|
finalType === "macrostop" ? "Macrostop Detected" :
|
||||||
finalType === "microstop" ? "Microstop Detected" :
|
finalType === "microstop" ? "Microstop Detected" :
|
||||||
"Event");
|
"Event");
|
||||||
|
|
||||||
const description = (ev as any).description ? String((ev as any).description) : null;
|
const description = clampText((ev as any).description, 1000);
|
||||||
|
|
||||||
// store full blob, ensure object
|
// store full blob, ensure object
|
||||||
const rawData = (ev as any).data ?? ev;
|
const rawData = (ev as any).data ?? ev;
|
||||||
@@ -144,7 +195,7 @@ export async function POST(req: Request) {
|
|||||||
orgId: machine.orgId,
|
orgId: machine.orgId,
|
||||||
machineId: machine.id,
|
machineId: machine.id,
|
||||||
ts,
|
ts,
|
||||||
topic: String((ev as any).topic ?? finalType),
|
topic: clampText((ev as any).topic ?? finalType, 64) ?? finalType,
|
||||||
eventType: finalType,
|
eventType: finalType,
|
||||||
severity: sev,
|
severity: sev,
|
||||||
requiresAck: !!(ev as any).requires_ack,
|
requiresAck: !!(ev as any).requires_ack,
|
||||||
@@ -152,13 +203,13 @@ export async function POST(req: Request) {
|
|||||||
description,
|
description,
|
||||||
data: dataObj,
|
data: dataObj,
|
||||||
workOrderId:
|
workOrderId:
|
||||||
(ev as any)?.work_order_id ? String((ev as any).work_order_id)
|
clampText((ev as any)?.work_order_id, 64) ??
|
||||||
: (ev as any)?.data?.work_order_id ? String((ev as any).data.work_order_id)
|
clampText((ev as any)?.data?.work_order_id, 64) ??
|
||||||
: null,
|
null,
|
||||||
sku:
|
sku:
|
||||||
(ev as any)?.sku ? String((ev as any).sku)
|
clampText((ev as any)?.sku, 64) ??
|
||||||
: (ev as any)?.data?.sku ? String((ev as any).data.sku)
|
clampText((ev as any)?.data?.sku, 64) ??
|
||||||
: null,
|
null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import type { NextRequest } from "next/server";
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { buildSessionCookieOptions, COOKIE_NAME, SESSION_DAYS } from "@/lib/auth/sessionCookie";
|
import { buildSessionCookieOptions, COOKIE_NAME, SESSION_DAYS } from "@/lib/auth/sessionCookie";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const tokenSchema = z.string().regex(/^[a-f0-9]{48}$/i);
|
||||||
|
const acceptSchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(80).optional(),
|
||||||
|
password: z.string().min(8).max(256),
|
||||||
|
});
|
||||||
|
|
||||||
async function loadInvite(token: string) {
|
async function loadInvite(token: string) {
|
||||||
return prisma.orgInvite.findFirst({
|
return prisma.orgInvite.findFirst({
|
||||||
@@ -23,6 +30,9 @@ export async function GET(
|
|||||||
{ params }: { params: Promise<{ token: string }> }
|
{ params }: { params: Promise<{ token: string }> }
|
||||||
) {
|
) {
|
||||||
const { token } = await params;
|
const { token } = await params;
|
||||||
|
if (!tokenSchema.safeParse(token).success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid invite token" }, { status: 400 });
|
||||||
|
}
|
||||||
const invite = await loadInvite(token);
|
const invite = await loadInvite(token);
|
||||||
if (!invite) {
|
if (!invite) {
|
||||||
return NextResponse.json({ ok: false, error: "Invite not found" }, { status: 404 });
|
return NextResponse.json({ ok: false, error: "Invite not found" }, { status: 404 });
|
||||||
@@ -44,23 +54,26 @@ export async function POST(
|
|||||||
{ params }: { params: Promise<{ token: string }> }
|
{ params }: { params: Promise<{ token: string }> }
|
||||||
) {
|
) {
|
||||||
const { token } = await params;
|
const { token } = await params;
|
||||||
|
if (!tokenSchema.safeParse(token).success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid invite token" }, { status: 400 });
|
||||||
|
}
|
||||||
const invite = await loadInvite(token);
|
const invite = await loadInvite(token);
|
||||||
if (!invite) {
|
if (!invite) {
|
||||||
return NextResponse.json({ ok: false, error: "Invite not found" }, { status: 404 });
|
return NextResponse.json({ ok: false, error: "Invite not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await req.json().catch(() => ({}));
|
const body = await req.json().catch(() => ({}));
|
||||||
const name = String(body.name || "").trim();
|
const parsed = acceptSchema.safeParse(body);
|
||||||
const password = String(body.password || "");
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid invite payload" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const name = String(parsed.data.name || "").trim();
|
||||||
|
const password = parsed.data.password;
|
||||||
|
|
||||||
const existingUser = await prisma.user.findUnique({
|
const existingUser = await prisma.user.findUnique({
|
||||||
where: { email: invite.email },
|
where: { email: invite.email },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!password || password.length < 8) {
|
|
||||||
return NextResponse.json({ ok: false, error: "Password must be at least 8 characters" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingUser && !name) {
|
if (!existingUser && !name) {
|
||||||
return NextResponse.json({ ok: false, error: "Name is required" }, { status: 400 });
|
return NextResponse.json({ ok: false, error: "Name is required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const COOKIE_NAME = "mis_session";
|
const COOKIE_NAME = "mis_session";
|
||||||
const SESSION_DAYS = 7;
|
const SESSION_DAYS = 7;
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z.string().trim().min(1).max(254).email(),
|
||||||
|
password: z.string().min(1).max(256),
|
||||||
|
next: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function safeNextPath(value: unknown) {
|
||||||
|
const raw = String(value ?? "").trim();
|
||||||
|
if (!raw) return "/machines";
|
||||||
|
if (!raw.startsWith("/") || raw.startsWith("//")) return "/machines";
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const body = await req.json().catch(() => ({}));
|
const body = await req.json().catch(() => ({}));
|
||||||
const email = String(body.email || "").trim().toLowerCase();
|
const parsed = loginSchema.safeParse(body);
|
||||||
const password = String(body.password || "");
|
if (!parsed.success) {
|
||||||
const next = String(body.next || "/machines");
|
return NextResponse.json({ ok: false, error: "Invalid login payload" }, { status: 400 });
|
||||||
|
|
||||||
if (!email || !password) {
|
|
||||||
return NextResponse.json({ ok: false, error: "Missing email/password" }, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
const email = parsed.data.email.toLowerCase();
|
||||||
|
const password = parsed.data.password;
|
||||||
|
const next = safeNextPath(parsed.data.next);
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({ where: { email } });
|
const user = await prisma.user.findUnique({ where: { email } });
|
||||||
if (!user || !user.isActive) {
|
if (!user || !user.isActive) {
|
||||||
|
|||||||
@@ -2,16 +2,30 @@ import { NextResponse } from "next/server";
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { buildSessionCookieOptions, COOKIE_NAME, SESSION_DAYS } from "@/lib/auth/sessionCookie";
|
import { buildSessionCookieOptions, COOKIE_NAME, SESSION_DAYS } from "@/lib/auth/sessionCookie";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z.string().trim().min(1).max(254).email(),
|
||||||
|
password: z.string().min(1).max(256),
|
||||||
|
next: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function safeNextPath(value: unknown) {
|
||||||
|
const raw = String(value ?? "").trim();
|
||||||
|
if (!raw) return "/machines";
|
||||||
|
if (!raw.startsWith("/") || raw.startsWith("//")) return "/machines";
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const body = await req.json().catch(() => ({}));
|
const body = await req.json().catch(() => ({}));
|
||||||
const email = String(body.email || "").trim().toLowerCase();
|
const parsed = loginSchema.safeParse(body);
|
||||||
const password = String(body.password || "");
|
if (!parsed.success) {
|
||||||
const next = String(body.next || "/machines");
|
return NextResponse.json({ ok: false, error: "Invalid login payload" }, { status: 400 });
|
||||||
|
|
||||||
if (!email || !password) {
|
|
||||||
return NextResponse.json({ ok: false, error: "Missing email/password" }, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
const email = parsed.data.email.toLowerCase();
|
||||||
|
const password = parsed.data.password;
|
||||||
|
const next = safeNextPath(parsed.data.next);
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({ where: { email } });
|
const user = await prisma.user.findUnique({ where: { email } });
|
||||||
if (!user || !user.isActive) {
|
if (!user || !user.isActive) {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import type { NextRequest } from "next/server";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { requireSession } from "@/lib/auth/requireSession";
|
import { requireSession } from "@/lib/auth/requireSession";
|
||||||
|
|
||||||
function normalizeEvent(row: any) {
|
function normalizeEvent(
|
||||||
|
row: any,
|
||||||
|
thresholds: { microMultiplier: number; macroMultiplier: number }
|
||||||
|
) {
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// 1) Parse row.data safely
|
// 1) Parse row.data safely
|
||||||
// data may be:
|
// data may be:
|
||||||
@@ -91,9 +94,24 @@ function normalizeEvent(row: any) {
|
|||||||
(typeof inner?.stop_duration_seconds === "number" && inner.stop_duration_seconds) ||
|
(typeof inner?.stop_duration_seconds === "number" && inner.stop_duration_seconds) ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
// tune these thresholds to match your MES spec
|
const microMultiplier = Number(thresholds?.microMultiplier ?? 1.5);
|
||||||
const MACROSTOP_SEC = 300; // 5 min
|
const macroMultiplier = Math.max(
|
||||||
eventType = stopSec != null && stopSec >= MACROSTOP_SEC ? "macrostop" : "microstop";
|
microMultiplier,
|
||||||
|
Number(thresholds?.macroMultiplier ?? 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
const theoreticalCycle =
|
||||||
|
Number(inner?.theoretical_cycle_time ?? blob?.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";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
@@ -203,6 +221,17 @@ export async function GET(
|
|||||||
return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const orgSettings = await prisma.orgSettings.findUnique({
|
||||||
|
where: { orgId: session.orgId },
|
||||||
|
select: { stoppageMultiplier: true, macroStoppageMultiplier: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const microMultiplier = Number(orgSettings?.stoppageMultiplier ?? 1.5);
|
||||||
|
const macroMultiplier = Math.max(
|
||||||
|
microMultiplier,
|
||||||
|
Number(orgSettings?.macroStoppageMultiplier ?? 5)
|
||||||
|
);
|
||||||
|
|
||||||
const rawEvents = await prisma.machineEvent.findMany({
|
const rawEvents = await prisma.machineEvent.findMany({
|
||||||
where: {
|
where: {
|
||||||
orgId: session.orgId,
|
orgId: session.orgId,
|
||||||
@@ -224,7 +253,9 @@ export async function GET(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalized = rawEvents.map(normalizeEvent);
|
const normalized = rawEvents.map((row) =>
|
||||||
|
normalizeEvent(row, { microMultiplier, macroMultiplier })
|
||||||
|
);
|
||||||
|
|
||||||
const ALLOWED_TYPES = new Set([
|
const ALLOWED_TYPES = new Set([
|
||||||
"slow-cycle",
|
"slow-cycle",
|
||||||
@@ -308,14 +339,15 @@ const cycles = rawCycles
|
|||||||
location: machine.location,
|
location: machine.location,
|
||||||
latestHeartbeat: machine.heartbeats[0] ?? null,
|
latestHeartbeat: machine.heartbeats[0] ?? null,
|
||||||
latestKpi: machine.kpiSnapshots[0] ?? null,
|
latestKpi: machine.kpiSnapshots[0] ?? null,
|
||||||
effectiveCycleTime
|
effectiveCycleTime,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
thresholds: {
|
||||||
|
stoppageMultiplier: microMultiplier,
|
||||||
|
macroStoppageMultiplier: macroMultiplier,
|
||||||
|
},
|
||||||
events,
|
events,
|
||||||
cycles
|
cycles
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,20 @@ import { randomBytes } from "crypto";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { getBaseUrl } from "@/lib/appUrl";
|
import { getBaseUrl } from "@/lib/appUrl";
|
||||||
import { normalizePairingCode } from "@/lib/pairingCode";
|
import { normalizePairingCode } from "@/lib/pairingCode";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const pairSchema = z.object({
|
||||||
|
code: z.string().trim().max(16).optional(),
|
||||||
|
pairingCode: z.string().trim().max(16).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const body = await req.json().catch(() => ({}));
|
const body = await req.json().catch(() => ({}));
|
||||||
const rawCode = String(body.code || body.pairingCode || "").trim();
|
const parsed = pairSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid pairing payload" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const rawCode = String(parsed.data.code || parsed.data.pairingCode || "").trim();
|
||||||
const code = normalizePairingCode(rawCode);
|
const code = normalizePairingCode(rawCode);
|
||||||
|
|
||||||
if (!code || code.length !== 5) {
|
if (!code || code.length !== 5) {
|
||||||
|
|||||||
@@ -3,9 +3,16 @@ import { randomBytes } from "crypto";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { generatePairingCode } from "@/lib/pairingCode";
|
import { generatePairingCode } from "@/lib/pairingCode";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const COOKIE_NAME = "mis_session";
|
const COOKIE_NAME = "mis_session";
|
||||||
|
|
||||||
|
const createMachineSchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(80),
|
||||||
|
code: z.string().trim().max(40).optional(),
|
||||||
|
location: z.string().trim().max(80).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
async function requireSession() {
|
async function requireSession() {
|
||||||
const sessionId = (await cookies()).get(COOKIE_NAME)?.value;
|
const sessionId = (await cookies()).get(COOKIE_NAME)?.value;
|
||||||
if (!sessionId) return null;
|
if (!sessionId) return null;
|
||||||
@@ -79,14 +86,15 @@ export async function POST(req: Request) {
|
|||||||
if (!session) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const body = await req.json().catch(() => ({}));
|
const body = await req.json().catch(() => ({}));
|
||||||
const name = String(body.name || "").trim();
|
const parsed = createMachineSchema.safeParse(body);
|
||||||
const codeRaw = String(body.code || "").trim();
|
if (!parsed.success) {
|
||||||
const locationRaw = String(body.location || "").trim();
|
return NextResponse.json({ ok: false, error: "Invalid machine payload" }, { status: 400 });
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
return NextResponse.json({ ok: false, error: "Machine name is required" }, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const name = parsed.data.name;
|
||||||
|
const codeRaw = parsed.data.code ?? "";
|
||||||
|
const locationRaw = parsed.data.location ?? "";
|
||||||
|
|
||||||
const existing = await prisma.machine.findFirst({
|
const existing = await prisma.machine.findFirst({
|
||||||
where: { orgId: session.orgId, name },
|
where: { orgId: session.orgId, name },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import { NextResponse } from "next/server";
|
|||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { requireSession } from "@/lib/auth/requireSession";
|
import { requireSession } from "@/lib/auth/requireSession";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
function canManageMembers(role?: string | null) {
|
function canManageMembers(role?: string | null) {
|
||||||
return role === "OWNER" || role === "ADMIN";
|
return role === "OWNER" || role === "ADMIN";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inviteIdSchema = z.string().uuid();
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
_req: NextRequest,
|
_req: NextRequest,
|
||||||
{ params }: { params: Promise<{ inviteId: string }> }
|
{ params }: { params: Promise<{ inviteId: string }> }
|
||||||
@@ -17,6 +20,9 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
const { inviteId } = await params;
|
const { inviteId } = await params;
|
||||||
|
if (!inviteIdSchema.safeParse(inviteId).success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid invite id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const membership = await prisma.orgUser.findUnique({
|
const membership = await prisma.orgUser.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import { prisma } from "@/lib/prisma";
|
|||||||
import { requireSession } from "@/lib/auth/requireSession";
|
import { requireSession } from "@/lib/auth/requireSession";
|
||||||
import { buildInviteEmail, sendEmail } from "@/lib/email";
|
import { buildInviteEmail, sendEmail } from "@/lib/email";
|
||||||
import { getBaseUrl } from "@/lib/appUrl";
|
import { getBaseUrl } from "@/lib/appUrl";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const INVITE_DAYS = 7;
|
const INVITE_DAYS = 7;
|
||||||
const ROLES = new Set(["OWNER", "ADMIN", "MEMBER"]);
|
const ROLES = new Set(["OWNER", "ADMIN", "MEMBER"]);
|
||||||
|
const inviteSchema = z.object({
|
||||||
|
email: z.string().trim().min(1).max(254).email(),
|
||||||
|
role: z.string().trim().toUpperCase().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
function canManageMembers(role?: string | null) {
|
function canManageMembers(role?: string | null) {
|
||||||
return role === "OWNER" || role === "ADMIN";
|
return role === "OWNER" || role === "ADMIN";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidEmail(email: string) {
|
|
||||||
return email.includes("@") && email.includes(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -97,12 +98,12 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await req.json().catch(() => ({}));
|
const body = await req.json().catch(() => ({}));
|
||||||
const email = String(body.email || "").trim().toLowerCase();
|
const parsed = inviteSchema.safeParse(body);
|
||||||
const role = String(body.role || "MEMBER").toUpperCase();
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid invite payload" }, { status: 400 });
|
||||||
if (!email || !isValidEmail(email)) {
|
|
||||||
return NextResponse.json({ ok: false, error: "Invalid email" }, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
const email = parsed.data.email.toLowerCase();
|
||||||
|
const role = String(parsed.data.role || "MEMBER").toUpperCase();
|
||||||
|
|
||||||
if (!ROLES.has(role)) {
|
if (!ROLES.has(role)) {
|
||||||
return NextResponse.json({ ok: false, error: "Invalid role" }, { status: 400 });
|
return NextResponse.json({ ok: false, error: "Invalid role" }, { status: 400 });
|
||||||
|
|||||||
@@ -15,11 +15,25 @@ import {
|
|||||||
validateShiftSchedule,
|
validateShiftSchedule,
|
||||||
validateThresholds,
|
validateThresholds,
|
||||||
} from "@/lib/settings";
|
} from "@/lib/settings";
|
||||||
|
import { publishSettingsUpdate } from "@/lib/mqtt";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
function isPlainObject(value: any): value is Record<string, any> {
|
function isPlainObject(value: any): value is Record<string, any> {
|
||||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canManageSettings(role?: string | null) {
|
||||||
|
return role === "OWNER" || role === "ADMIN";
|
||||||
|
}
|
||||||
|
|
||||||
|
const machineIdSchema = z.string().uuid();
|
||||||
|
const machineSettingsSchema = z
|
||||||
|
.object({
|
||||||
|
source: z.string().trim().max(40).optional(),
|
||||||
|
overrides: z.any().optional(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
function pickAllowedOverrides(raw: any) {
|
function pickAllowedOverrides(raw: any) {
|
||||||
if (!isPlainObject(raw)) return {};
|
if (!isPlainObject(raw)) return {};
|
||||||
const out: Record<string, any> = {};
|
const out: Record<string, any> = {};
|
||||||
@@ -29,7 +43,11 @@ function pickAllowedOverrides(raw: any) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureOrgSettings(tx: Prisma.TransactionClient, orgId: string, userId: string) {
|
async function ensureOrgSettings(
|
||||||
|
tx: Prisma.TransactionClient,
|
||||||
|
orgId: string,
|
||||||
|
userId?: string | null
|
||||||
|
) {
|
||||||
let settings = await tx.orgSettings.findUnique({
|
let settings = await tx.orgSettings.findUnique({
|
||||||
where: { orgId },
|
where: { orgId },
|
||||||
});
|
});
|
||||||
@@ -65,12 +83,13 @@ async function ensureOrgSettings(tx: Prisma.TransactionClient, orgId: string, us
|
|||||||
shiftChangeCompMin: 10,
|
shiftChangeCompMin: 10,
|
||||||
lunchBreakMin: 30,
|
lunchBreakMin: 30,
|
||||||
stoppageMultiplier: 1.5,
|
stoppageMultiplier: 1.5,
|
||||||
|
macroStoppageMultiplier: 5,
|
||||||
oeeAlertThresholdPct: 90,
|
oeeAlertThresholdPct: 90,
|
||||||
performanceThresholdPct: 85,
|
performanceThresholdPct: 85,
|
||||||
qualitySpikeDeltaPct: 5,
|
qualitySpikeDeltaPct: 5,
|
||||||
alertsJson: DEFAULT_ALERTS,
|
alertsJson: DEFAULT_ALERTS,
|
||||||
defaultsJson: DEFAULT_DEFAULTS,
|
defaultsJson: DEFAULT_DEFAULTS,
|
||||||
updatedBy: userId,
|
updatedBy: userId ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,23 +112,40 @@ async function ensureOrgSettings(tx: Prisma.TransactionClient, orgId: string, us
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ machineId: string }> }
|
{ params }: { params: Promise<{ machineId: string }> }
|
||||||
) {
|
) {
|
||||||
const session = await requireSession();
|
|
||||||
if (!session) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
|
||||||
|
|
||||||
const { machineId } = await params;
|
const { machineId } = await params;
|
||||||
|
if (!machineIdSchema.safeParse(machineId).success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid machine id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const machine = await prisma.machine.findFirst({
|
const session = await requireSession();
|
||||||
where: { id: machineId, orgId: session.orgId },
|
let orgId: string | null = null;
|
||||||
select: { id: true },
|
let userId: string | null = null;
|
||||||
});
|
let machine: { id: string; orgId: string } | null = null;
|
||||||
|
|
||||||
if (!machine) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
if (session) {
|
||||||
|
machine = await prisma.machine.findFirst({
|
||||||
|
where: { id: machineId, orgId: session.orgId },
|
||||||
|
select: { id: true, orgId: true },
|
||||||
|
});
|
||||||
|
if (!machine) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
||||||
|
orgId = machine.orgId;
|
||||||
|
userId = session.userId;
|
||||||
|
} else {
|
||||||
|
const apiKey = req.headers.get("x-api-key");
|
||||||
|
if (!apiKey) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
machine = await prisma.machine.findFirst({
|
||||||
|
where: { id: machineId, apiKey },
|
||||||
|
select: { id: true, orgId: true },
|
||||||
|
});
|
||||||
|
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
orgId = machine.orgId;
|
||||||
|
}
|
||||||
|
|
||||||
const { settings, overrides } = await prisma.$transaction(async (tx) => {
|
const { settings, overrides } = await prisma.$transaction(async (tx) => {
|
||||||
const orgSettings = await ensureOrgSettings(tx, session.orgId, session.userId);
|
const orgSettings = await ensureOrgSettings(tx, orgId as string, userId);
|
||||||
if (!orgSettings?.settings) throw new Error("SETTINGS_NOT_FOUND");
|
if (!orgSettings?.settings) throw new Error("SETTINGS_NOT_FOUND");
|
||||||
|
|
||||||
const machineSettings = await tx.machineSettings.findUnique({
|
const machineSettings = await tx.machineSettings.findUnique({
|
||||||
@@ -140,7 +176,18 @@ export async function PUT(
|
|||||||
const session = await requireSession();
|
const session = await requireSession();
|
||||||
if (!session) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
const membership = await prisma.orgUser.findUnique({
|
||||||
|
where: { orgId_userId: { orgId: session.orgId, userId: session.userId } },
|
||||||
|
select: { role: true },
|
||||||
|
});
|
||||||
|
if (!canManageSettings(membership?.role)) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const { machineId } = await params;
|
const { machineId } = await params;
|
||||||
|
if (!machineIdSchema.safeParse(machineId).success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid machine id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const machine = await prisma.machine.findFirst({
|
const machine = await prisma.machine.findFirst({
|
||||||
where: { id: machineId, orgId: session.orgId },
|
where: { id: machineId, orgId: session.orgId },
|
||||||
@@ -150,9 +197,13 @@ export async function PUT(
|
|||||||
if (!machine) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
if (!machine) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
||||||
|
|
||||||
const body = await req.json().catch(() => ({}));
|
const body = await req.json().catch(() => ({}));
|
||||||
const source = String(body.source ?? "control_tower");
|
const parsed = machineSettingsSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid settings payload" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const source = String(parsed.data.source ?? "control_tower");
|
||||||
|
|
||||||
let patch = body.overrides ?? body;
|
let patch = parsed.data.overrides ?? parsed.data;
|
||||||
if (patch === null) {
|
if (patch === null) {
|
||||||
patch = null;
|
patch = null;
|
||||||
}
|
}
|
||||||
@@ -238,16 +289,20 @@ export async function PUT(
|
|||||||
if (patch?.thresholds) {
|
if (patch?.thresholds) {
|
||||||
patch = {
|
patch = {
|
||||||
...patch,
|
...patch,
|
||||||
thresholds: {
|
thresholds: {
|
||||||
...patch.thresholds,
|
...patch.thresholds,
|
||||||
stoppageMultiplier:
|
stoppageMultiplier:
|
||||||
patch.thresholds.stoppageMultiplier !== undefined
|
patch.thresholds.stoppageMultiplier !== undefined
|
||||||
? Number(patch.thresholds.stoppageMultiplier)
|
? Number(patch.thresholds.stoppageMultiplier)
|
||||||
: patch.thresholds.stoppageMultiplier,
|
: patch.thresholds.stoppageMultiplier,
|
||||||
oeeAlertThresholdPct:
|
macroStoppageMultiplier:
|
||||||
patch.thresholds.oeeAlertThresholdPct !== undefined
|
patch.thresholds.macroStoppageMultiplier !== undefined
|
||||||
? Number(patch.thresholds.oeeAlertThresholdPct)
|
? Number(patch.thresholds.macroStoppageMultiplier)
|
||||||
: patch.thresholds.oeeAlertThresholdPct,
|
: patch.thresholds.macroStoppageMultiplier,
|
||||||
|
oeeAlertThresholdPct:
|
||||||
|
patch.thresholds.oeeAlertThresholdPct !== undefined
|
||||||
|
? Number(patch.thresholds.oeeAlertThresholdPct)
|
||||||
|
: patch.thresholds.oeeAlertThresholdPct,
|
||||||
performanceThresholdPct:
|
performanceThresholdPct:
|
||||||
patch.thresholds.performanceThresholdPct !== undefined
|
patch.thresholds.performanceThresholdPct !== undefined
|
||||||
? Number(patch.thresholds.performanceThresholdPct)
|
? Number(patch.thresholds.performanceThresholdPct)
|
||||||
@@ -318,9 +373,30 @@ export async function PUT(
|
|||||||
const overrides = pickAllowedOverrides(saved.overridesJson ?? {});
|
const overrides = pickAllowedOverrides(saved.overridesJson ?? {});
|
||||||
const effective = deepMerge(orgPayload, overrides);
|
const effective = deepMerge(orgPayload, overrides);
|
||||||
|
|
||||||
return { orgPayload, overrides, effective };
|
return {
|
||||||
|
orgPayload,
|
||||||
|
overrides,
|
||||||
|
effective,
|
||||||
|
overridesUpdatedAt: saved.updatedAt,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const overridesUpdatedAt =
|
||||||
|
result.overridesUpdatedAt && result.overridesUpdatedAt instanceof Date
|
||||||
|
? result.overridesUpdatedAt.toISOString()
|
||||||
|
: undefined;
|
||||||
|
try {
|
||||||
|
await publishSettingsUpdate({
|
||||||
|
orgId: session.orgId,
|
||||||
|
machineId,
|
||||||
|
version: Number(result.orgPayload.version ?? 0),
|
||||||
|
source,
|
||||||
|
overridesUpdatedAt,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[settings machine PUT] MQTT publish failed", err);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
machineId,
|
machineId,
|
||||||
|
|||||||
@@ -16,11 +16,29 @@ import {
|
|||||||
validateShiftSchedule,
|
validateShiftSchedule,
|
||||||
validateThresholds,
|
validateThresholds,
|
||||||
} from "@/lib/settings";
|
} from "@/lib/settings";
|
||||||
|
import { publishSettingsUpdate } from "@/lib/mqtt";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
function isPlainObject(value: any): value is Record<string, any> {
|
function isPlainObject(value: any): value is Record<string, any> {
|
||||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canManageSettings(role?: string | null) {
|
||||||
|
return role === "OWNER" || role === "ADMIN";
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsPayloadSchema = z
|
||||||
|
.object({
|
||||||
|
source: z.string().trim().max(40).optional(),
|
||||||
|
timezone: z.string().trim().max(64).optional(),
|
||||||
|
shiftSchedule: z.any().optional(),
|
||||||
|
thresholds: z.any().optional(),
|
||||||
|
alerts: z.any().optional(),
|
||||||
|
defaults: z.any().optional(),
|
||||||
|
version: z.union([z.number(), z.string()]).optional(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
async function ensureOrgSettings(tx: Prisma.TransactionClient, orgId: string, userId: string) {
|
async function ensureOrgSettings(tx: Prisma.TransactionClient, orgId: string, userId: string) {
|
||||||
let settings = await tx.orgSettings.findUnique({
|
let settings = await tx.orgSettings.findUnique({
|
||||||
where: { orgId },
|
where: { orgId },
|
||||||
@@ -57,6 +75,7 @@ async function ensureOrgSettings(tx: Prisma.TransactionClient, orgId: string, us
|
|||||||
shiftChangeCompMin: 10,
|
shiftChangeCompMin: 10,
|
||||||
lunchBreakMin: 30,
|
lunchBreakMin: 30,
|
||||||
stoppageMultiplier: 1.5,
|
stoppageMultiplier: 1.5,
|
||||||
|
macroStoppageMultiplier: 5,
|
||||||
oeeAlertThresholdPct: 90,
|
oeeAlertThresholdPct: 90,
|
||||||
performanceThresholdPct: 85,
|
performanceThresholdPct: 85,
|
||||||
qualitySpikeDeltaPct: 5,
|
qualitySpikeDeltaPct: 5,
|
||||||
@@ -108,15 +127,28 @@ export async function PUT(req: Request) {
|
|||||||
const session = await requireSession();
|
const session = await requireSession();
|
||||||
if (!session) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
const membership = await prisma.orgUser.findUnique({
|
||||||
|
where: { orgId_userId: { orgId: session.orgId, userId: session.userId } },
|
||||||
|
select: { role: true },
|
||||||
|
});
|
||||||
|
if (!canManageSettings(membership?.role)) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await req.json().catch(() => ({}));
|
const body = await req.json().catch(() => ({}));
|
||||||
const source = String(body.source ?? "control_tower");
|
const parsed = settingsPayloadSchema.safeParse(body);
|
||||||
const timezone = body.timezone;
|
if (!parsed.success) {
|
||||||
const shiftSchedule = body.shiftSchedule;
|
return NextResponse.json({ ok: false, error: "Invalid settings payload" }, { status: 400 });
|
||||||
const thresholds = body.thresholds;
|
}
|
||||||
const alerts = body.alerts;
|
|
||||||
const defaults = body.defaults;
|
const source = String(parsed.data.source ?? "control_tower");
|
||||||
const expectedVersion = body.version;
|
const timezone = parsed.data.timezone;
|
||||||
|
const shiftSchedule = parsed.data.shiftSchedule;
|
||||||
|
const thresholds = parsed.data.thresholds;
|
||||||
|
const alerts = parsed.data.alerts;
|
||||||
|
const defaults = parsed.data.defaults;
|
||||||
|
const expectedVersion = parsed.data.version;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
timezone === undefined &&
|
timezone === undefined &&
|
||||||
@@ -192,6 +224,10 @@ export async function PUT(req: Request) {
|
|||||||
shiftSchedule?.lunchBreakMin !== undefined ? Number(shiftSchedule.lunchBreakMin) : undefined,
|
shiftSchedule?.lunchBreakMin !== undefined ? Number(shiftSchedule.lunchBreakMin) : undefined,
|
||||||
stoppageMultiplier:
|
stoppageMultiplier:
|
||||||
thresholds?.stoppageMultiplier !== undefined ? Number(thresholds.stoppageMultiplier) : undefined,
|
thresholds?.stoppageMultiplier !== undefined ? Number(thresholds.stoppageMultiplier) : undefined,
|
||||||
|
macroStoppageMultiplier:
|
||||||
|
thresholds?.macroStoppageMultiplier !== undefined
|
||||||
|
? Number(thresholds.macroStoppageMultiplier)
|
||||||
|
: undefined,
|
||||||
oeeAlertThresholdPct:
|
oeeAlertThresholdPct:
|
||||||
thresholds?.oeeAlertThresholdPct !== undefined ? Number(thresholds.oeeAlertThresholdPct) : undefined,
|
thresholds?.oeeAlertThresholdPct !== undefined ? Number(thresholds.oeeAlertThresholdPct) : undefined,
|
||||||
performanceThresholdPct:
|
performanceThresholdPct:
|
||||||
@@ -267,6 +303,22 @@ export async function PUT(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload = buildSettingsPayload(updated.settings, updated.shifts ?? []);
|
const payload = buildSettingsPayload(updated.settings, updated.shifts ?? []);
|
||||||
|
const updatedAt =
|
||||||
|
typeof payload.updatedAt === "string"
|
||||||
|
? payload.updatedAt
|
||||||
|
: payload.updatedAt
|
||||||
|
? payload.updatedAt.toISOString()
|
||||||
|
: undefined;
|
||||||
|
try {
|
||||||
|
await publishSettingsUpdate({
|
||||||
|
orgId: session.orgId,
|
||||||
|
version: Number(payload.version ?? 0),
|
||||||
|
source,
|
||||||
|
updatedAt,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[settings PUT] MQTT publish failed", err);
|
||||||
|
}
|
||||||
return NextResponse.json({ ok: true, settings: payload });
|
return NextResponse.json({ ok: true, settings: payload });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[settings PUT] failed", err);
|
console.error("[settings PUT] failed", err);
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ import { DEFAULT_ALERTS, DEFAULT_DEFAULTS, DEFAULT_SHIFT } from "@/lib/settings"
|
|||||||
import { buildVerifyEmail, sendEmail } from "@/lib/email";
|
import { buildVerifyEmail, sendEmail } from "@/lib/email";
|
||||||
import { getBaseUrl } from "@/lib/appUrl";
|
import { getBaseUrl } from "@/lib/appUrl";
|
||||||
import { logLine } from "@/lib/logger";
|
import { logLine } from "@/lib/logger";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const signupSchema = z.object({
|
||||||
|
orgName: z.string().trim().min(1).max(120),
|
||||||
|
name: z.string().trim().min(1).max(80),
|
||||||
|
email: z.string().trim().min(1).max(254).email(),
|
||||||
|
password: z.string().min(8).max(256),
|
||||||
|
});
|
||||||
|
|
||||||
function slugify(input: string) {
|
function slugify(input: string) {
|
||||||
const trimmed = input.trim().toLowerCase();
|
const trimmed = input.trim().toLowerCase();
|
||||||
@@ -16,28 +23,16 @@ function slugify(input: string) {
|
|||||||
return slug || "org";
|
return slug || "org";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidEmail(email: string) {
|
|
||||||
return email.includes("@") && email.includes(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const body = await req.json().catch(() => ({}));
|
const body = await req.json().catch(() => ({}));
|
||||||
const orgName = String(body.orgName || "").trim();
|
const parsed = signupSchema.safeParse(body);
|
||||||
const name = String(body.name || "").trim();
|
if (!parsed.success) {
|
||||||
const email = String(body.email || "").trim().toLowerCase();
|
return NextResponse.json({ ok: false, error: "Invalid signup payload" }, { status: 400 });
|
||||||
const password = String(body.password || "");
|
|
||||||
|
|
||||||
if (!orgName || !name || !email || !password) {
|
|
||||||
return NextResponse.json({ ok: false, error: "Missing required fields" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValidEmail(email)) {
|
|
||||||
return NextResponse.json({ ok: false, error: "Invalid email" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 8) {
|
|
||||||
return NextResponse.json({ ok: false, error: "Password must be at least 8 characters" }, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
const orgName = parsed.data.orgName;
|
||||||
|
const name = parsed.data.name;
|
||||||
|
const email = parsed.data.email.toLowerCase();
|
||||||
|
const password = parsed.data.password;
|
||||||
|
|
||||||
const existing = await prisma.user.findUnique({ where: { email } });
|
const existing = await prisma.user.findUnique({ where: { email } });
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -86,6 +81,7 @@ export async function POST(req: Request) {
|
|||||||
shiftChangeCompMin: 10,
|
shiftChangeCompMin: 10,
|
||||||
lunchBreakMin: 30,
|
lunchBreakMin: 30,
|
||||||
stoppageMultiplier: 1.5,
|
stoppageMultiplier: 1.5,
|
||||||
|
macroStoppageMultiplier: 5,
|
||||||
oeeAlertThresholdPct: 90,
|
oeeAlertThresholdPct: 90,
|
||||||
performanceThresholdPct: 85,
|
performanceThresholdPct: 85,
|
||||||
qualitySpikeDeltaPct: 5,
|
qualitySpikeDeltaPct: 5,
|
||||||
|
|||||||
49
app/api/work-orders/machines/[machineId]/route.ts
Normal file
49
app/api/work-orders/machines/[machineId]/route.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { requireSession } from "@/lib/auth/requireSession";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ machineId: string }> }
|
||||||
|
) {
|
||||||
|
const { machineId } = await params;
|
||||||
|
|
||||||
|
const session = await requireSession();
|
||||||
|
let orgId: string | null = null;
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
const machine = await prisma.machine.findFirst({
|
||||||
|
where: { id: machineId, orgId: session.orgId },
|
||||||
|
select: { id: true, orgId: true },
|
||||||
|
});
|
||||||
|
if (!machine) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
||||||
|
orgId = machine.orgId;
|
||||||
|
} else {
|
||||||
|
const apiKey = req.headers.get("x-api-key");
|
||||||
|
if (!apiKey) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
const machine = await prisma.machine.findFirst({
|
||||||
|
where: { id: machineId, apiKey },
|
||||||
|
select: { id: true, orgId: true },
|
||||||
|
});
|
||||||
|
if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
orgId = machine.orgId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await prisma.machineWorkOrder.findMany({
|
||||||
|
where: { machineId, orgId: orgId as string, status: { not: "DONE" } },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
machineId,
|
||||||
|
workOrders: rows.map((row) => ({
|
||||||
|
workOrderId: row.workOrderId,
|
||||||
|
sku: row.sku,
|
||||||
|
targetQty: row.targetQty,
|
||||||
|
cycleTime: row.cycleTime,
|
||||||
|
status: row.status,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
164
app/api/work-orders/route.ts
Normal file
164
app/api/work-orders/route.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { requireSession } from "@/lib/auth/requireSession";
|
||||||
|
import { publishWorkOrdersUpdate } from "@/lib/mqtt";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
function canManage(role?: string | null) {
|
||||||
|
return role === "OWNER" || role === "ADMIN";
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_WORK_ORDERS = 2000;
|
||||||
|
const MAX_WORK_ORDER_ID_LENGTH = 64;
|
||||||
|
const MAX_SKU_LENGTH = 64;
|
||||||
|
const MAX_TARGET_QTY = 2_000_000_000;
|
||||||
|
const MAX_CYCLE_TIME = 86_400;
|
||||||
|
const WORK_ORDER_ID_RE = /^[A-Za-z0-9._-]+$/;
|
||||||
|
|
||||||
|
const uploadBodySchema = z.object({
|
||||||
|
machineId: z.string().trim().min(1),
|
||||||
|
workOrders: z.array(z.any()).optional(),
|
||||||
|
orders: z.array(z.any()).optional(),
|
||||||
|
workOrder: z.any().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function cleanText(value: unknown, maxLen: number) {
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
const text = String(value).trim();
|
||||||
|
if (!text) return null;
|
||||||
|
const sanitized = text.replace(/[\u0000-\u001f\u007f]/g, "");
|
||||||
|
if (!sanitized) return null;
|
||||||
|
return sanitized.length > maxLen ? sanitized.slice(0, maxLen) : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIntOrNull(value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === "") return null;
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n)) return null;
|
||||||
|
return Math.trunc(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFloatOrNull(value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === "") return null;
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n)) return null;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkOrderInput = {
|
||||||
|
workOrderId: string;
|
||||||
|
sku?: string | null;
|
||||||
|
targetQty?: number | null;
|
||||||
|
cycleTime?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeWorkOrders(raw: any[]) {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const cleaned: WorkOrderInput[] = [];
|
||||||
|
|
||||||
|
for (const item of raw) {
|
||||||
|
const idRaw = cleanText(item?.workOrderId ?? item?.id ?? item?.work_order_id, MAX_WORK_ORDER_ID_LENGTH);
|
||||||
|
if (!idRaw || !WORK_ORDER_ID_RE.test(idRaw) || seen.has(idRaw)) continue;
|
||||||
|
seen.add(idRaw);
|
||||||
|
|
||||||
|
const sku = cleanText(item?.sku ?? item?.SKU ?? null, MAX_SKU_LENGTH);
|
||||||
|
const targetQtyRaw = toIntOrNull(item?.targetQty ?? item?.target_qty ?? item?.target ?? item?.targetQuantity);
|
||||||
|
const cycleTimeRaw = toFloatOrNull(
|
||||||
|
item?.cycleTime ?? item?.theoreticalCycleTime ?? item?.theoretical_cycle_time ?? item?.cycle_time
|
||||||
|
);
|
||||||
|
const targetQty =
|
||||||
|
targetQtyRaw == null ? null : Math.min(Math.max(targetQtyRaw, 0), MAX_TARGET_QTY);
|
||||||
|
const cycleTime =
|
||||||
|
cycleTimeRaw == null ? null : Math.min(Math.max(cycleTimeRaw, 0), MAX_CYCLE_TIME);
|
||||||
|
|
||||||
|
cleaned.push({
|
||||||
|
workOrderId: idRaw,
|
||||||
|
sku: sku ?? null,
|
||||||
|
targetQty: targetQty ?? null,
|
||||||
|
cycleTime: cycleTime ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const session = await requireSession();
|
||||||
|
if (!session) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
const membership = await prisma.orgUser.findUnique({
|
||||||
|
where: { orgId_userId: { orgId: session.orgId, userId: session.userId } },
|
||||||
|
select: { role: true },
|
||||||
|
});
|
||||||
|
if (!canManage(membership?.role)) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json().catch(() => ({}));
|
||||||
|
const parsedBody = uploadBodySchema.safeParse(body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Invalid payload" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const machineId = String(parsedBody.data.machineId ?? "").trim();
|
||||||
|
if (!machineId) {
|
||||||
|
return NextResponse.json({ ok: false, error: "machineId is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const machine = await prisma.machine.findFirst({
|
||||||
|
where: { id: machineId, orgId: session.orgId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!machine) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
||||||
|
|
||||||
|
const listRaw = Array.isArray(parsedBody.data.workOrders)
|
||||||
|
? parsedBody.data.workOrders
|
||||||
|
: Array.isArray(parsedBody.data.orders)
|
||||||
|
? parsedBody.data.orders
|
||||||
|
: parsedBody.data.workOrder
|
||||||
|
? [parsedBody.data.workOrder]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (listRaw.length > MAX_WORK_ORDERS) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ ok: false, error: `Too many work orders (max ${MAX_WORK_ORDERS})` },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = normalizeWorkOrders(listRaw);
|
||||||
|
if (!cleaned.length) {
|
||||||
|
return NextResponse.json({ ok: false, error: "No valid work orders provided" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await prisma.machineWorkOrder.createMany({
|
||||||
|
data: cleaned.map((row) => ({
|
||||||
|
orgId: session.orgId,
|
||||||
|
machineId,
|
||||||
|
workOrderId: row.workOrderId,
|
||||||
|
sku: row.sku ?? null,
|
||||||
|
targetQty: row.targetQty ?? null,
|
||||||
|
cycleTime: row.cycleTime ?? null,
|
||||||
|
status: "PENDING",
|
||||||
|
})),
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await publishWorkOrdersUpdate({
|
||||||
|
orgId: session.orgId,
|
||||||
|
machineId,
|
||||||
|
count: created.count,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[work orders POST] MQTT publish failed", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
machineId,
|
||||||
|
inserted: created.count,
|
||||||
|
total: cleaned.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -178,6 +178,14 @@ Main KPIs remain English in ES-MX (OEE, KPI, SKU, AVAILABILITY, PERFORMANCE, QUA
|
|||||||
| machine.detail.error.failed | Failed to load machine | No se pudo cargar la máquina |
|
| machine.detail.error.failed | Failed to load machine | No se pudo cargar la máquina |
|
||||||
| machine.detail.error.network | Network error | Error de red |
|
| machine.detail.error.network | Network error | Error de red |
|
||||||
| machine.detail.back | Back | Volver |
|
| machine.detail.back | Back | Volver |
|
||||||
|
| machine.detail.workOrders.upload | Upload Work Orders | Subir ordenes de trabajo |
|
||||||
|
| machine.detail.workOrders.uploading | Uploading... | Subiendo... |
|
||||||
|
| machine.detail.workOrders.uploadParsing | Parsing file... | Leyendo archivo... |
|
||||||
|
| machine.detail.workOrders.uploadHint | CSV or XLSX with Work Order ID, SKU, Theoretical Cycle Time (Seconds), Target Quantity. | CSV o XLSX con Work Order ID, SKU, Theoretical Cycle Time (Seconds), Target Quantity. |
|
||||||
|
| machine.detail.workOrders.uploadSuccess | Uploaded {count} work orders | Se cargaron {count} ordenes de trabajo |
|
||||||
|
| machine.detail.workOrders.uploadError | Upload failed | No se pudo cargar |
|
||||||
|
| machine.detail.workOrders.uploadInvalid | No valid work orders found | No se encontraron ordenes de trabajo validas |
|
||||||
|
| machine.detail.workOrders.uploadUnauthorized | Not authorized to upload work orders | No autorizado para cargar ordenes de trabajo |
|
||||||
| machine.detail.status.offline | OFFLINE | FUERA DE LÍNEA |
|
| machine.detail.status.offline | OFFLINE | FUERA DE LÍNEA |
|
||||||
| machine.detail.status.unknown | UNKNOWN | DESCONOCIDO |
|
| machine.detail.status.unknown | UNKNOWN | DESCONOCIDO |
|
||||||
| machine.detail.status.run | RUN | EN MARCHA |
|
| machine.detail.status.run | RUN | EN MARCHA |
|
||||||
@@ -334,6 +342,7 @@ Main KPIs remain English in ES-MX (OEE, KPI, SKU, AVAILABILITY, PERFORMANCE, QUA
|
|||||||
| settings.thresholds.stoppage | Stoppage multiplier | Multiplicador de paro |
|
| settings.thresholds.stoppage | Stoppage multiplier | Multiplicador de paro |
|
||||||
| settings.alerts | Alerts | Alertas |
|
| settings.alerts | Alerts | Alertas |
|
||||||
| settings.alertsSubtitle | Choose which alerts to notify. | Elige qué alertas notificar. |
|
| settings.alertsSubtitle | Choose which alerts to notify. | Elige qué alertas notificar. |
|
||||||
|
| settings.thresholds.macroStoppage | Macro stoppage multiplier | Multiplicador de macroparo |
|
||||||
| settings.alerts.oeeDrop | OEE drop alerts | Alertas por caída de OEE |
|
| settings.alerts.oeeDrop | OEE drop alerts | Alertas por caída de OEE |
|
||||||
| settings.alerts.oeeDropHelper | Notify when OEE falls below threshold | Notificar cuando OEE esté por debajo del umbral |
|
| settings.alerts.oeeDropHelper | Notify when OEE falls below threshold | Notificar cuando OEE esté por debajo del umbral |
|
||||||
| settings.alerts.performanceDegradation | Performance degradation alerts | Alertas por baja de Performance |
|
| settings.alerts.performanceDegradation | Performance degradation alerts | Alertas por baja de Performance |
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
"machines.empty": "No machines found for this org.",
|
"machines.empty": "No machines found for this org.",
|
||||||
"machines.status": "Status",
|
"machines.status": "Status",
|
||||||
"machines.status.noHeartbeat": "No heartbeat",
|
"machines.status.noHeartbeat": "No heartbeat",
|
||||||
"machines.status.ok": "OK",
|
"machines.status.ok": "Heartbeat",
|
||||||
"machines.status.offline": "OFFLINE",
|
"machines.status.offline": "OFFLINE",
|
||||||
"machines.status.unknown": "UNKNOWN",
|
"machines.status.unknown": "UNKNOWN",
|
||||||
"machines.lastSeen": "Last seen {time}",
|
"machines.lastSeen": "Last seen {time}",
|
||||||
@@ -140,6 +140,14 @@
|
|||||||
"machine.detail.error.failed": "Failed to load machine",
|
"machine.detail.error.failed": "Failed to load machine",
|
||||||
"machine.detail.error.network": "Network error",
|
"machine.detail.error.network": "Network error",
|
||||||
"machine.detail.back": "Back",
|
"machine.detail.back": "Back",
|
||||||
|
"machine.detail.workOrders.upload": "Upload Work Orders",
|
||||||
|
"machine.detail.workOrders.uploading": "Uploading...",
|
||||||
|
"machine.detail.workOrders.uploadParsing": "Parsing file...",
|
||||||
|
"machine.detail.workOrders.uploadHint": "CSV or XLSX with Work Order ID, SKU, Theoretical Cycle Time (Seconds), Target Quantity.",
|
||||||
|
"machine.detail.workOrders.uploadSuccess": "Uploaded {count} work orders",
|
||||||
|
"machine.detail.workOrders.uploadError": "Upload failed",
|
||||||
|
"machine.detail.workOrders.uploadInvalid": "No valid work orders found",
|
||||||
|
"machine.detail.workOrders.uploadUnauthorized": "Not authorized to upload work orders",
|
||||||
"machine.detail.status.offline": "OFFLINE",
|
"machine.detail.status.offline": "OFFLINE",
|
||||||
"machine.detail.status.unknown": "UNKNOWN",
|
"machine.detail.status.unknown": "UNKNOWN",
|
||||||
"machine.detail.status.run": "RUN",
|
"machine.detail.status.run": "RUN",
|
||||||
@@ -286,6 +294,7 @@
|
|||||||
"settings.thresholds.performance": "Performance threshold",
|
"settings.thresholds.performance": "Performance threshold",
|
||||||
"settings.thresholds.qualitySpike": "Quality spike delta",
|
"settings.thresholds.qualitySpike": "Quality spike delta",
|
||||||
"settings.thresholds.stoppage": "Stoppage multiplier",
|
"settings.thresholds.stoppage": "Stoppage multiplier",
|
||||||
|
"settings.thresholds.macroStoppage": "Macro stoppage multiplier",
|
||||||
"settings.alerts": "Alerts",
|
"settings.alerts": "Alerts",
|
||||||
"settings.alertsSubtitle": "Choose which alerts to notify.",
|
"settings.alertsSubtitle": "Choose which alerts to notify.",
|
||||||
"settings.alerts.oeeDrop": "OEE drop alerts",
|
"settings.alerts.oeeDrop": "OEE drop alerts",
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
"machines.empty": "No se encontraron máquinas para esta organización.",
|
"machines.empty": "No se encontraron máquinas para esta organización.",
|
||||||
"machines.status": "Estado",
|
"machines.status": "Estado",
|
||||||
"machines.status.noHeartbeat": "Sin heartbeat",
|
"machines.status.noHeartbeat": "Sin heartbeat",
|
||||||
"machines.status.ok": "OK",
|
"machines.status.ok": "Latido",
|
||||||
"machines.status.offline": "FUERA DE LÍNEA",
|
"machines.status.offline": "FUERA DE LÍNEA",
|
||||||
"machines.status.unknown": "DESCONOCIDO",
|
"machines.status.unknown": "DESCONOCIDO",
|
||||||
"machines.lastSeen": "Visto hace {time}",
|
"machines.lastSeen": "Visto hace {time}",
|
||||||
@@ -140,6 +140,14 @@
|
|||||||
"machine.detail.error.failed": "No se pudo cargar la máquina",
|
"machine.detail.error.failed": "No se pudo cargar la máquina",
|
||||||
"machine.detail.error.network": "Error de red",
|
"machine.detail.error.network": "Error de red",
|
||||||
"machine.detail.back": "Volver",
|
"machine.detail.back": "Volver",
|
||||||
|
"machine.detail.workOrders.upload": "Subir ordenes de trabajo",
|
||||||
|
"machine.detail.workOrders.uploading": "Subiendo...",
|
||||||
|
"machine.detail.workOrders.uploadParsing": "Leyendo archivo...",
|
||||||
|
"machine.detail.workOrders.uploadHint": "CSV o XLSX con Work Order ID, SKU, Theoretical Cycle Time (Seconds), Target Quantity.",
|
||||||
|
"machine.detail.workOrders.uploadSuccess": "Se cargaron {count} ordenes de trabajo",
|
||||||
|
"machine.detail.workOrders.uploadError": "No se pudo cargar",
|
||||||
|
"machine.detail.workOrders.uploadInvalid": "No se encontraron ordenes de trabajo validas",
|
||||||
|
"machine.detail.workOrders.uploadUnauthorized": "No autorizado para cargar ordenes de trabajo",
|
||||||
"machine.detail.status.offline": "FUERA DE LÍNEA",
|
"machine.detail.status.offline": "FUERA DE LÍNEA",
|
||||||
"machine.detail.status.unknown": "DESCONOCIDO",
|
"machine.detail.status.unknown": "DESCONOCIDO",
|
||||||
"machine.detail.status.run": "EN MARCHA",
|
"machine.detail.status.run": "EN MARCHA",
|
||||||
@@ -286,6 +294,7 @@
|
|||||||
"settings.thresholds.performance": "Umbral de Performance",
|
"settings.thresholds.performance": "Umbral de Performance",
|
||||||
"settings.thresholds.qualitySpike": "Delta de pico de calidad",
|
"settings.thresholds.qualitySpike": "Delta de pico de calidad",
|
||||||
"settings.thresholds.stoppage": "Multiplicador de paro",
|
"settings.thresholds.stoppage": "Multiplicador de paro",
|
||||||
|
"settings.thresholds.macroStoppage": "Multiplicador de macroparo",
|
||||||
"settings.alerts": "Alertas",
|
"settings.alerts": "Alertas",
|
||||||
"settings.alertsSubtitle": "Elige qué alertas notificar.",
|
"settings.alertsSubtitle": "Elige qué alertas notificar.",
|
||||||
"settings.alerts.oeeDrop": "Alertas por caída de OEE",
|
"settings.alerts.oeeDrop": "Alertas por caída de OEE",
|
||||||
|
|||||||
124
lib/mqtt.ts
Normal file
124
lib/mqtt.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import mqtt, { MqttClient } from "mqtt";
|
||||||
|
|
||||||
|
type SettingsUpdate = {
|
||||||
|
orgId: string;
|
||||||
|
version: number;
|
||||||
|
source?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
machineId?: string;
|
||||||
|
overridesUpdatedAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkOrdersUpdate = {
|
||||||
|
orgId: string;
|
||||||
|
machineId: string;
|
||||||
|
count?: number;
|
||||||
|
source?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MQTT_URL = process.env.MQTT_BROKER_URL || "";
|
||||||
|
const MQTT_USERNAME = process.env.MQTT_USERNAME;
|
||||||
|
const MQTT_PASSWORD = process.env.MQTT_PASSWORD;
|
||||||
|
const MQTT_CLIENT_ID = process.env.MQTT_CLIENT_ID;
|
||||||
|
const MQTT_TOPIC_PREFIX = (process.env.MQTT_TOPIC_PREFIX || "mis").replace(/\/+$/, "");
|
||||||
|
const MQTT_QOS_RAW = Number(process.env.MQTT_QOS ?? "2");
|
||||||
|
const MQTT_QOS = MQTT_QOS_RAW === 0 || MQTT_QOS_RAW === 1 || MQTT_QOS_RAW === 2 ? MQTT_QOS_RAW : 2;
|
||||||
|
|
||||||
|
let client: MqttClient | null = null;
|
||||||
|
let connecting: Promise<MqttClient> | null = null;
|
||||||
|
|
||||||
|
function buildSettingsTopic(orgId: string, machineId?: string) {
|
||||||
|
const base = `${MQTT_TOPIC_PREFIX}/org/${orgId}`;
|
||||||
|
if (machineId) return `${base}/machines/${machineId}/settings/updated`;
|
||||||
|
return `${base}/settings/updated`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkOrdersTopic(orgId: string, machineId: string) {
|
||||||
|
const base = `${MQTT_TOPIC_PREFIX}/org/${orgId}`;
|
||||||
|
return `${base}/machines/${machineId}/work_orders/updated`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getClient() {
|
||||||
|
if (!MQTT_URL) return null;
|
||||||
|
if (client?.connected) return client;
|
||||||
|
if (connecting) return connecting;
|
||||||
|
|
||||||
|
connecting = new Promise((resolve, reject) => {
|
||||||
|
const next = mqtt.connect(MQTT_URL, {
|
||||||
|
clientId: MQTT_CLIENT_ID,
|
||||||
|
username: MQTT_USERNAME,
|
||||||
|
password: MQTT_PASSWORD,
|
||||||
|
clean: true,
|
||||||
|
reconnectPeriod: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
next.once("connect", () => {
|
||||||
|
client = next;
|
||||||
|
connecting = null;
|
||||||
|
resolve(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
next.once("error", (err) => {
|
||||||
|
next.end(true);
|
||||||
|
client = null;
|
||||||
|
connecting = null;
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
next.once("close", () => {
|
||||||
|
client = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return connecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishSettingsUpdate(update: SettingsUpdate) {
|
||||||
|
if (!MQTT_URL) return { ok: false, reason: "MQTT_NOT_CONFIGURED" as const };
|
||||||
|
const mqttClient = await getClient();
|
||||||
|
if (!mqttClient) return { ok: false, reason: "MQTT_NOT_CONFIGURED" as const };
|
||||||
|
|
||||||
|
const topic = buildSettingsTopic(update.orgId, update.machineId);
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
type: update.machineId ? "machine_settings_updated" : "org_settings_updated",
|
||||||
|
orgId: update.orgId,
|
||||||
|
machineId: update.machineId,
|
||||||
|
version: update.version,
|
||||||
|
source: update.source || "control_tower",
|
||||||
|
updatedAt: update.updatedAt,
|
||||||
|
overridesUpdatedAt: update.overridesUpdatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise<{ ok: true }>((resolve, reject) => {
|
||||||
|
mqttClient.publish(topic, payload, { qos: MQTT_QOS }, (err) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve({ ok: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishWorkOrdersUpdate(update: WorkOrdersUpdate) {
|
||||||
|
if (!MQTT_URL) return { ok: false, reason: "MQTT_NOT_CONFIGURED" as const };
|
||||||
|
const mqttClient = await getClient();
|
||||||
|
if (!mqttClient) return { ok: false, reason: "MQTT_NOT_CONFIGURED" as const };
|
||||||
|
|
||||||
|
const topic = buildWorkOrdersTopic(update.orgId, update.machineId);
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
type: "work_orders_updated",
|
||||||
|
orgId: update.orgId,
|
||||||
|
machineId: update.machineId,
|
||||||
|
count: update.count ?? null,
|
||||||
|
source: update.source || "control_tower",
|
||||||
|
updatedAt: update.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise<{ ok: true }>((resolve, reject) => {
|
||||||
|
mqttClient.publish(topic, payload, { qos: MQTT_QOS }, (err) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve({ ok: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ export function buildSettingsPayload(settings: any, shifts: any[]) {
|
|||||||
},
|
},
|
||||||
thresholds: {
|
thresholds: {
|
||||||
stoppageMultiplier: settings.stoppageMultiplier,
|
stoppageMultiplier: settings.stoppageMultiplier,
|
||||||
|
macroStoppageMultiplier: settings.macroStoppageMultiplier,
|
||||||
oeeAlertThresholdPct: settings.oeeAlertThresholdPct,
|
oeeAlertThresholdPct: settings.oeeAlertThresholdPct,
|
||||||
performanceThresholdPct: settings.performanceThresholdPct,
|
performanceThresholdPct: settings.performanceThresholdPct,
|
||||||
qualitySpikeDeltaPct: settings.qualitySpikeDeltaPct,
|
qualitySpikeDeltaPct: settings.qualitySpikeDeltaPct,
|
||||||
@@ -159,6 +160,14 @@ export function validateThresholds(thresholds: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const macroStoppage = thresholds.macroStoppageMultiplier;
|
||||||
|
if (macroStoppage != null) {
|
||||||
|
const v = Number(macroStoppage);
|
||||||
|
if (!Number.isFinite(v) || v < 1.1 || v > 20.0) {
|
||||||
|
return { ok: false, error: "macroStoppageMultiplier must be 1.1-20.0" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const oee = thresholds.oeeAlertThresholdPct;
|
const oee = thresholds.oeeAlertThresholdPct;
|
||||||
if (oee != null) {
|
if (oee != null) {
|
||||||
const v = Number(oee);
|
const v = Number(oee);
|
||||||
|
|||||||
576
package-lock.json
generated
576
package-lock.json
generated
@@ -12,11 +12,13 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"i18n": "^0.15.3",
|
"i18n": "^0.15.3",
|
||||||
"lucide-react": "^0.561.0",
|
"lucide-react": "^0.561.0",
|
||||||
|
"mqtt": "^5.10.0",
|
||||||
"next": "16.0.10",
|
"next": "16.0.10",
|
||||||
"nodemailer": "^7.0.12",
|
"nodemailer": "^7.0.12",
|
||||||
"react": "19.2.1",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.1",
|
"react-dom": "19.2.1",
|
||||||
"recharts": "^3.6.0",
|
"recharts": "^3.6.0",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -984,6 +986,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||||
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.27.2",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||||
@@ -3175,7 +3186,6 @@
|
|||||||
"version": "20.19.27",
|
"version": "20.19.27",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
||||||
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
@@ -3212,12 +3222,30 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/readable-stream": {
|
||||||
|
"version": "4.0.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz",
|
||||||
|
"integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/use-sync-external-store": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.50.0",
|
"version": "8.50.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
|
||||||
@@ -3756,6 +3784,18 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
@@ -3779,6 +3819,15 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@@ -4049,6 +4098,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.9",
|
"version": "2.9.9",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.9.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.9.tgz",
|
||||||
@@ -4073,6 +4142,18 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "6.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
||||||
|
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/readable-stream": "^4.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bowser": {
|
"node_modules/bowser": {
|
||||||
"version": "2.13.1",
|
"version": "2.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
|
||||||
@@ -4104,6 +4185,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/broker-factory": {
|
||||||
|
"version": "3.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.11.tgz",
|
||||||
|
"integrity": "sha512-ex4RuEI0AJOdaIcXe1lu9EqRAVkoYvdcvwLvNcE5UZQzYNqzPY+z0frnlxT4+cUwNVpE//9MwGx4lKiLH+pEcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"fast-unique-numbers": "^9.0.24",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-factory": "^7.0.46"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.1",
|
"version": "4.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||||
@@ -4138,6 +4231,36 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/c12": {
|
"node_modules/c12": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
||||||
@@ -4247,6 +4370,19 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -4305,6 +4441,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -4325,6 +4470,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/commist": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -4332,6 +4483,35 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/concat-stream/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/confbox": {
|
"node_modules/confbox": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
|
||||||
@@ -4356,6 +4536,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -5429,12 +5621,30 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
@@ -5525,6 +5735,19 @@
|
|||||||
"node": ">=10.0"
|
"node": ">=10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-unique-numbers": {
|
||||||
|
"version": "9.0.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.24.tgz",
|
||||||
|
"integrity": "sha512-Dv0BYn4waOWse94j16rsZ5w/0zoaCa74O3q6IZjMqaXbtT92Q+Sb6pPk+phGzD8Xh+nueQmSRI3tSCaHKidzKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-xml-parser": {
|
"node_modules/fast-xml-parser": {
|
||||||
"version": "5.2.5",
|
"version": "5.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
||||||
@@ -5634,6 +5857,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -5940,6 +6172,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/help-me": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/hermes-estree": {
|
"node_modules/hermes-estree": {
|
||||||
"version": "0.25.1",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||||
@@ -5977,6 +6215,26 @@
|
|||||||
"url": "https://github.com/sponsors/mashpie"
|
"url": "https://github.com/sponsors/mashpie"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -6024,6 +6282,12 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||||
@@ -6048,6 +6312,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -6505,6 +6778,16 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-sdsl": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/js-sdsl"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -7024,7 +7307,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -7036,6 +7318,55 @@
|
|||||||
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
|
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/mqtt": {
|
||||||
|
"version": "5.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.14.1.tgz",
|
||||||
|
"integrity": "sha512-NxkPxE70Uq3Ph7goefQa7ggSsVzHrayCD0OyxlJgITN/EbzlZN+JEPmaAZdxP1LsIT5FamDyILoQTF72W7Nnbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/readable-stream": "^4.0.21",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"commist": "^3.2.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"debug": "^4.4.1",
|
||||||
|
"help-me": "^5.0.0",
|
||||||
|
"lru-cache": "^10.4.3",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"mqtt-packet": "^9.0.2",
|
||||||
|
"number-allocator": "^1.0.14",
|
||||||
|
"readable-stream": "^4.7.0",
|
||||||
|
"rfdc": "^1.4.1",
|
||||||
|
"socks": "^2.8.6",
|
||||||
|
"split2": "^4.2.0",
|
||||||
|
"worker-timers": "^8.0.23",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mqtt": "build/bin/mqtt.js",
|
||||||
|
"mqtt_pub": "build/bin/pub.js",
|
||||||
|
"mqtt_sub": "build/bin/sub.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt-packet": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^6.0.8",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"process-nextick-args": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -7215,6 +7546,16 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/number-allocator": {
|
||||||
|
"version": "1.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
|
||||||
|
"integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.1",
|
||||||
|
"js-sdsl": "4.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nypm": {
|
"node_modules/nypm": {
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
|
||||||
@@ -7593,6 +7934,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -7714,6 +8070,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -7875,6 +8247,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rfdc": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -7919,6 +8297,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-identifier": {
|
"node_modules/safe-identifier": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz",
|
||||||
@@ -8182,6 +8580,30 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||||
|
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.0.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -8191,6 +8613,27 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||||
@@ -8212,6 +8655,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string.prototype.includes": {
|
"node_modules/string.prototype.includes": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||||
@@ -8644,6 +9096,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -8705,7 +9163,6 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unrs-resolver": {
|
"node_modules/unrs-resolver": {
|
||||||
@@ -8793,6 +9250,12 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/victory-vendor": {
|
"node_modules/victory-vendor": {
|
||||||
"version": "37.3.6",
|
"version": "37.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
@@ -8920,6 +9383,24 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
@@ -8930,6 +9411,95 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/worker-factory": {
|
||||||
|
"version": "7.0.46",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.46.tgz",
|
||||||
|
"integrity": "sha512-Sr1hq2FMgNa04UVhYQacsw+i58BtMimzDb4+CqYphZ97OfefRpURu0UZ+JxMr/H36VVJBfuVkxTK7MytsanC3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"fast-unique-numbers": "^9.0.24",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers": {
|
||||||
|
"version": "8.0.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.27.tgz",
|
||||||
|
"integrity": "sha512-+7ptDduAWj6Wd09Ga0weRFRx/MUwLhExazn+zu3IrwF0N2U2FPqFRR5W3Qz4scnI3cOILzdIEEytIJ2vbeD9Gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-timers-broker": "^8.0.13",
|
||||||
|
"worker-timers-worker": "^9.0.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers-broker": {
|
||||||
|
"version": "8.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.13.tgz",
|
||||||
|
"integrity": "sha512-PZnHHmqOY5oMKQPyfJhqPI9cb3QFmwD3lCIc/Zip6sShpfG2rvvCVDl0xeabGIspiEpP5exNNIlTUHjgP5VAcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"broker-factory": "^3.1.11",
|
||||||
|
"fast-unique-numbers": "^9.0.24",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-timers-worker": "^9.0.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers-worker": {
|
||||||
|
"version": "9.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.11.tgz",
|
||||||
|
"integrity": "sha512-pArb5xtgHWImYpXhjg1OFv7JFG0ubmccb73TFoXHXjG830fFj+16N57q9YeBnZX52dn+itRrMoJZ9HaZBVzDaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-factory": "^7.0.46"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -13,11 +13,13 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"i18n": "^0.15.3",
|
"i18n": "^0.15.3",
|
||||||
"lucide-react": "^0.561.0",
|
"lucide-react": "^0.561.0",
|
||||||
|
"mqtt": "^5.10.0",
|
||||||
"next": "16.0.10",
|
"next": "16.0.10",
|
||||||
"nodemailer": "^7.0.12",
|
"nodemailer": "^7.0.12",
|
||||||
"react": "19.2.1",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.1",
|
"react-dom": "19.2.1",
|
||||||
"recharts": "^3.6.0",
|
"recharts": "^3.6.0",
|
||||||
|
"xlsx": "^0.20.2",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "machine_work_orders" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"orgId" TEXT NOT NULL,
|
||||||
|
"machineId" TEXT NOT NULL,
|
||||||
|
"workOrderId" TEXT NOT NULL,
|
||||||
|
"sku" TEXT,
|
||||||
|
"targetQty" INTEGER,
|
||||||
|
"cycleTime" DOUBLE PRECISION,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "machine_work_orders_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "machine_work_orders_machineId_workOrderId_key" ON "machine_work_orders"("machineId", "workOrderId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "machine_work_orders_orgId_machineId_idx" ON "machine_work_orders"("orgId", "machineId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "machine_work_orders_orgId_workOrderId_idx" ON "machine_work_orders"("orgId", "workOrderId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "machine_work_orders" ADD CONSTRAINT "machine_work_orders_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "machine_work_orders" ADD CONSTRAINT "machine_work_orders_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES "Machine"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "org_settings" (
|
||||||
|
"org_id" TEXT NOT NULL,
|
||||||
|
"timezone" TEXT NOT NULL DEFAULT 'UTC',
|
||||||
|
"shift_change_comp_min" INTEGER NOT NULL DEFAULT 10,
|
||||||
|
"lunch_break_min" INTEGER NOT NULL DEFAULT 30,
|
||||||
|
"stoppage_multiplier" DOUBLE PRECISION NOT NULL DEFAULT 1.5,
|
||||||
|
"oee_alert_threshold_pct" DOUBLE PRECISION NOT NULL DEFAULT 90,
|
||||||
|
"macro_stoppage_multiplier" DOUBLE PRECISION NOT NULL DEFAULT 5,
|
||||||
|
"performance_threshold_pct" DOUBLE PRECISION NOT NULL DEFAULT 85,
|
||||||
|
"quality_spike_delta_pct" DOUBLE PRECISION NOT NULL DEFAULT 5,
|
||||||
|
"alerts_json" JSONB,
|
||||||
|
"defaults_json" JSONB,
|
||||||
|
"version" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"updated_by" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "org_settings_pkey" PRIMARY KEY ("org_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "org_shifts" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"org_id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"start_time" TEXT NOT NULL,
|
||||||
|
"end_time" TEXT NOT NULL,
|
||||||
|
"sort_order" INTEGER NOT NULL,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
|
CONSTRAINT "org_shifts_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "machine_settings" (
|
||||||
|
"machine_id" TEXT NOT NULL,
|
||||||
|
"org_id" TEXT NOT NULL,
|
||||||
|
"overrides_json" JSONB,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"updated_by" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "machine_settings_pkey" PRIMARY KEY ("machine_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "settings_audit" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"org_id" TEXT NOT NULL,
|
||||||
|
"machine_id" TEXT,
|
||||||
|
"actor_id" TEXT,
|
||||||
|
"source" TEXT NOT NULL,
|
||||||
|
"payload_json" JSONB NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "settings_audit_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "org_shifts_org_id_idx" ON "org_shifts"("org_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "org_shifts_org_id_sort_order_idx" ON "org_shifts"("org_id", "sort_order");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "machine_settings_org_id_idx" ON "machine_settings"("org_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "settings_audit_org_id_created_at_idx" ON "settings_audit"("org_id", "created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "settings_audit_machine_id_created_at_idx" ON "settings_audit"("machine_id", "created_at");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "org_settings" ADD CONSTRAINT "org_settings_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "org_shifts" ADD CONSTRAINT "org_shifts_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "machine_settings" ADD CONSTRAINT "machine_settings_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "machine_settings" ADD CONSTRAINT "machine_settings_machine_id_fkey" FOREIGN KEY ("machine_id") REFERENCES "Machine"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "settings_audit" ADD CONSTRAINT "settings_audit_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "settings_audit" ADD CONSTRAINT "settings_audit_machine_id_fkey" FOREIGN KEY ("machine_id") REFERENCES "Machine"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ model Org {
|
|||||||
heartbeats MachineHeartbeat[]
|
heartbeats MachineHeartbeat[]
|
||||||
kpiSnapshots MachineKpiSnapshot[]
|
kpiSnapshots MachineKpiSnapshot[]
|
||||||
events MachineEvent[]
|
events MachineEvent[]
|
||||||
|
workOrders MachineWorkOrder[]
|
||||||
settings OrgSettings?
|
settings OrgSettings?
|
||||||
shifts OrgShift[]
|
shifts OrgShift[]
|
||||||
machineSettings MachineSettings[]
|
machineSettings MachineSettings[]
|
||||||
@@ -119,6 +120,7 @@ model Machine {
|
|||||||
kpiSnapshots MachineKpiSnapshot[]
|
kpiSnapshots MachineKpiSnapshot[]
|
||||||
events MachineEvent[]
|
events MachineEvent[]
|
||||||
cycles MachineCycle[]
|
cycles MachineCycle[]
|
||||||
|
workOrders MachineWorkOrder[]
|
||||||
settings MachineSettings?
|
settings MachineSettings?
|
||||||
settingsAudits SettingsAudit[]
|
settingsAudits SettingsAudit[]
|
||||||
|
|
||||||
@@ -239,6 +241,27 @@ model MachineCycle {
|
|||||||
@@index([orgId, machineId, cycleCount])
|
@@index([orgId, machineId, cycleCount])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model MachineWorkOrder {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
orgId String
|
||||||
|
machineId String
|
||||||
|
workOrderId String
|
||||||
|
sku String?
|
||||||
|
targetQty Int?
|
||||||
|
cycleTime Float?
|
||||||
|
status String @default("PENDING")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
org Org @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||||
|
machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([machineId, workOrderId])
|
||||||
|
@@index([orgId, machineId])
|
||||||
|
@@index([orgId, workOrderId])
|
||||||
|
@@map("machine_work_orders")
|
||||||
|
}
|
||||||
|
|
||||||
model IngestLog {
|
model IngestLog {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
orgId String?
|
orgId String?
|
||||||
@@ -269,6 +292,7 @@ model OrgSettings {
|
|||||||
lunchBreakMin Int @default(30) @map("lunch_break_min")
|
lunchBreakMin Int @default(30) @map("lunch_break_min")
|
||||||
stoppageMultiplier Float @default(1.5) @map("stoppage_multiplier")
|
stoppageMultiplier Float @default(1.5) @map("stoppage_multiplier")
|
||||||
oeeAlertThresholdPct Float @default(90) @map("oee_alert_threshold_pct")
|
oeeAlertThresholdPct Float @default(90) @map("oee_alert_threshold_pct")
|
||||||
|
macroStoppageMultiplier Float @default(5) @map("macro_stoppage_multiplier")
|
||||||
performanceThresholdPct Float @default(85) @map("performance_threshold_pct")
|
performanceThresholdPct Float @default(85) @map("performance_threshold_pct")
|
||||||
qualitySpikeDeltaPct Float @default(5) @map("quality_spike_delta_pct")
|
qualitySpikeDeltaPct Float @default(5) @map("quality_spike_delta_pct")
|
||||||
alertsJson Json? @map("alerts_json")
|
alertsJson Json? @map("alerts_json")
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ async function main() {
|
|||||||
lunchBreakMin: 30,
|
lunchBreakMin: 30,
|
||||||
stoppageMultiplier: 1.5,
|
stoppageMultiplier: 1.5,
|
||||||
oeeAlertThresholdPct: 90,
|
oeeAlertThresholdPct: 90,
|
||||||
|
macroStoppageMultiplier: 5,
|
||||||
performanceThresholdPct: 85,
|
performanceThresholdPct: 85,
|
||||||
qualitySpikeDeltaPct: 5,
|
qualitySpikeDeltaPct: 5,
|
||||||
alertsJson: {
|
alertsJson: {
|
||||||
|
|||||||
Reference in New Issue
Block a user