diff --git a/426 b/426 new file mode 100644 index 0000000..e69de29 diff --git a/476 b/476 new file mode 100644 index 0000000..e69de29 diff --git a/app/(app)/recap/RecapPageSkeletons.tsx b/app/(app)/recap/RecapPageSkeletons.tsx new file mode 100644 index 0000000..c4e9f0e --- /dev/null +++ b/app/(app)/recap/RecapPageSkeletons.tsx @@ -0,0 +1,30 @@ +/** + * Shared markup for loading states (used by `loading.tsx` and explicit `` in pages) + * so the recap UI always shows the same skeleton while server data is pending. + */ +export function RecapGridPageSkeleton() { + return ( +
+
+
+ {Array.from({ length: 6 }).map((_, index) => ( +
+ ))} +
+
+ ); +} + +export function RecapDetailPageSkeleton() { + return ( +
+
+
+ {Array.from({ length: 4 }).map((_, index) => ( +
+ ))} +
+
+
+ ); +} diff --git a/app/(app)/recap/[machineId]/loading.tsx b/app/(app)/recap/[machineId]/loading.tsx index 78f55f0..e82357d 100644 --- a/app/(app)/recap/[machineId]/loading.tsx +++ b/app/(app)/recap/[machineId]/loading.tsx @@ -1,13 +1,5 @@ +import { RecapDetailPageSkeleton } from "../RecapPageSkeletons"; + export default function LoadingRecapDetail() { - return ( -
-
-
- {Array.from({ length: 4 }).map((_, index) => ( -
- ))} -
-
-
- ); + return ; } diff --git a/app/(app)/recap/[machineId]/page.tsx b/app/(app)/recap/[machineId]/page.tsx index 50950b5..c4731dc 100644 --- a/app/(app)/recap/[machineId]/page.tsx +++ b/app/(app)/recap/[machineId]/page.tsx @@ -1,9 +1,11 @@ +import { Suspense } from "react"; import { notFound, redirect } from "next/navigation"; import { requireSession } from "@/lib/auth/requireSession"; import { getRecapMachineDetailCached, parseRecapDetailRangeInput } from "@/lib/recap/redesign"; +import { RecapDetailPageSkeleton } from "../RecapPageSkeletons"; import RecapDetailClient from "./RecapDetailClient"; -export default async function RecapMachineDetailPage({ +async function RecapDetailData({ params, searchParams, }: { @@ -33,3 +35,17 @@ export default async function RecapMachineDetailPage({ /> ); } + +export default function RecapMachineDetailPage({ + params, + searchParams, +}: { + params: Promise<{ machineId: string }>; + searchParams?: Promise>; +}) { + return ( + }> + + + ); +} diff --git a/app/(app)/recap/loading.tsx b/app/(app)/recap/loading.tsx index 8dd6573..5232741 100644 --- a/app/(app)/recap/loading.tsx +++ b/app/(app)/recap/loading.tsx @@ -1,12 +1,5 @@ +import { RecapGridPageSkeleton } from "./RecapPageSkeletons"; + export default function LoadingRecapGrid() { - return ( -
-
-
- {Array.from({ length: 6 }).map((_, index) => ( -
- ))} -
-
- ); + return ; } diff --git a/app/(app)/recap/page.tsx b/app/(app)/recap/page.tsx index f0b6e13..fff45a0 100644 --- a/app/(app)/recap/page.tsx +++ b/app/(app)/recap/page.tsx @@ -1,9 +1,11 @@ +import { Suspense } from "react"; import { redirect } from "next/navigation"; import { requireSession } from "@/lib/auth/requireSession"; import { getRecapSummaryCached } from "@/lib/recap/redesign"; import RecapGridClient from "./RecapGridClient"; +import { RecapGridPageSkeleton } from "./RecapPageSkeletons"; -export default async function RecapPage() { +async function RecapGridData() { const session = await requireSession(); if (!session) redirect("/login?next=/recap"); @@ -14,3 +16,11 @@ export default async function RecapPage() { return ; } + +export default function RecapPage() { + return ( + }> + + + ); +} diff --git a/app/api/ingest/cycle/route.ts b/app/api/ingest/cycle/route.ts index 7505f77..4f97f56 100644 --- a/app/api/ingest/cycle/route.ts +++ b/app/api/ingest/cycle/route.ts @@ -172,11 +172,35 @@ export async function POST(req: Request) { }; }); + const result = await prisma.machineCycle.createMany({ + data: rows, + skipDuplicates: true, + }); + if (rows.length === 1) { - const row = await prisma.machineCycle.create({ data: rows[0] }); - return NextResponse.json({ ok: true, id: row.id, ts: row.ts }); + const row = await prisma.machineCycle.findFirst({ + where: { + orgId: machine.orgId, + machineId: machine.id, + ts: rows[0].ts, + cycleCount: rows[0].cycleCount ?? null, + }, + orderBy: { createdAt: "asc" }, + select: { id: true, ts: true }, + }); + return NextResponse.json({ + ok: true, + id: row?.id, + ts: row?.ts, + inserted: result.count, + duplicate: result.count === 0, + }); } - const result = await prisma.machineCycle.createMany({ data: rows }); - return NextResponse.json({ ok: true, count: result.count }); + return NextResponse.json({ + ok: true, + inserted: result.count, + requested: rows.length, + count: result.count, + }); } diff --git a/app/api/ingest/event/route.ts b/app/api/ingest/event/route.ts index 1c3418c..dbdfbdf 100644 --- a/app/api/ingest/event/route.ts +++ b/app/api/ingest/event/route.ts @@ -80,6 +80,15 @@ function numberFrom(value: unknown) { } return null; } +function parseSeqToBigInt(value: unknown): bigint | null { + if (value === null || value === undefined) return null; + if (typeof value === "number") { + if (!Number.isInteger(value) || value < 0) return null; + return BigInt(value); + } + if (typeof value === "string" && /^\d+$/.test(value)) return BigInt(value); + return null; +} function canonicalText(value: unknown) { return String(value ?? "") @@ -262,6 +271,8 @@ export async function POST(req: Request) { const machine = await getMachineAuth(String(machineId), apiKey); if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); + const bodySeq = parseSeqToBigInt(bodyRecord.seq); + const bodySchemaVersion = clampText(bodyRecord.schemaVersion, 16); const orgSettings = await prisma.orgSettings.findUnique({ where: { orgId: machine.orgId }, select: { stoppageMultiplier: true, macroStoppageMultiplier: true, defaultsJson: true }, @@ -410,35 +421,90 @@ export async function POST(req: Request) { const activeWorkOrder = asRecord(evRecord.activeWorkOrder); const dataActiveWorkOrder = asRecord(evData.activeWorkOrder); - const row = await prisma.machineEvent.create({ - data: { - orgId: machine.orgId, - machineId: machine.id, - ts, - topic: clampText(evRecord.topic ?? finalType, 64) ?? finalType, - eventType: finalType, - severity: sev, - requiresAck: !!evRecord.requires_ack, - title, - description, - data: toJsonValue(dataObj), - workOrderId: - clampText(evRecord.work_order_id, 64) ?? - clampText(evData.work_order_id, 64) ?? - clampText(activeWorkOrder?.id, 64) ?? - clampText(dataActiveWorkOrder?.id, 64) ?? - null, - sku: - clampText(evRecord.sku, 64) ?? - clampText(evData.sku, 64) ?? - clampText(activeWorkOrder?.sku, 64) ?? - clampText(dataActiveWorkOrder?.sku, 64) ?? - null, - }, + // ✨ Cada evento puede traer su propio seq, o usar el del payload raíz + const evSeq = + parseSeqToBigInt(evRecord.seq) ?? + parseSeqToBigInt(evData.seq) ?? + bodySeq; + + const evSchemaVersion = + clampText(evRecord.schemaVersion, 16) ?? + bodySchemaVersion; + + const eventData = { + orgId: machine.orgId, + machineId: machine.id, + schemaVersion: evSchemaVersion, + seq: evSeq, + ts, + topic: clampText(evRecord.topic ?? finalType, 64) ?? finalType, + eventType: finalType, + severity: sev, + requiresAck: !!evRecord.requires_ack, + title, + description, + data: toJsonValue(dataObj), + workOrderId: + clampText(evRecord.work_order_id, 64) ?? + clampText(evData.work_order_id, 64) ?? + clampText(activeWorkOrder?.id, 64) ?? + clampText(dataActiveWorkOrder?.id, 64) ?? + null, + sku: + clampText(evRecord.sku, 64) ?? + clampText(evData.sku, 64) ?? + clampText(activeWorkOrder?.sku, 64) ?? + clampText(dataActiveWorkOrder?.sku, 64) ?? + null, + }; + + // ✨ Idempotente: si ya existe (mismo orgId+machineId+seq), no inserta + const insertResult = await prisma.machineEvent.createMany({ + data: [eventData], + skipDuplicates: true, }); + // ✨ Buscar la fila (la recién creada o la duplicada existente) + let row; + if (evSeq != null) { + row = await prisma.machineEvent.findFirst({ + where: { + orgId: machine.orgId, + machineId: machine.id, + seq: evSeq, + }, + orderBy: { ts: "asc" }, + }); + } else { + // Sin seq, buscar por ts (fallback compatibilidad con eventos viejos) + row = await prisma.machineEvent.findFirst({ + where: { + orgId: machine.orgId, + machineId: machine.id, + ts, + eventType: finalType, + }, + orderBy: { ts: "desc" }, + }); + } + + if (!row) { + skipped.push({ reason: "row_not_found_after_insert", seq: evSeq?.toString() }); + continue; + } + + const wasDuplicate = insertResult.count === 0; + + // Si fue duplicado, no procesar reasonEntry ni alertas (ya se hicieron antes) + if (wasDuplicate) { + created.push({ id: row.id, ts: row.ts, eventType: row.eventType }); + continue; // ✨ saltar el resto del procesamiento + } + created.push({ id: row.id, ts: row.ts, eventType: row.eventType }); + + // If the payload carries a `reason`, create the corresponding ReasonEntry. // If it doesn't, still create an "UNCLASSIFIED" downtime ReasonEntry for stop events so the dashboard can show coverage. if (evRecord.is_update || evRecord.is_auto_ack || dataObj.is_update || dataObj.is_auto_ack){ diff --git a/app/api/ingest/event/route.ts.bak b/app/api/ingest/event/route.ts.bak new file mode 100644 index 0000000..9f3df16 --- /dev/null +++ b/app/api/ingest/event/route.ts.bak @@ -0,0 +1,670 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { getMachineAuth } from "@/lib/machineAuthCache"; +import { z } from "zod"; +import { evaluateAlertsForEvent } from "@/lib/alerts/engine"; +import { toJsonValue } from "@/lib/prismaJson"; +import { + findCatalogReason, + loadFallbackReasonCatalog, + normalizeReasonCatalog, + toReasonCode, + type ReasonCatalog, + type ReasonCatalogKind, +} from "@/lib/reasonCatalog"; + +const normalizeType = (t: unknown) => + String(t ?? "") + .trim() + .toLowerCase() + .replace(/_/g, "-"); + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record; +} + +const CANON_TYPE: Record = { + // Node-RED + "production-stopped": "stop", + "oee-drop": "oee-drop", + "quality-spike": "quality-spike", + "predictive-oee-decline": "predictive-oee-decline", + "performance-degradation": "performance-degradation", + + // legacy / synonyms + "macroparo": "macrostop", + "macro-stop": "macrostop", + "microparo": "microstop", + "micro-paro": "microstop", + "down": "stop", + "downtime-acknowledged": "downtime-acknowledged", + "scrap-manual-entry": "scrap-manual-entry", + "mold-change": "mold-change", +}; + +const ALLOWED_TYPES = new Set([ + "slow-cycle", + "microstop", + "macrostop", + "offline", + "error", + "oee-drop", + "quality-spike", + "performance-degradation", + "predictive-oee-decline", + "downtime-acknowledged", + "scrap-manual-entry", + "mold-change", +]); + +const machineIdSchema = z.string().uuid(); +const MAX_EVENTS = 100; + +//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; +} + +function numberFrom(value: unknown) { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string" && value.trim()) { + const parsed = Number(value); + if (Number.isFinite(parsed)) return parsed; + } + return null; +} +function parseSeqToBigInt(value: unknown): bigint | null { + if (value === null || value === undefined) return null; + if (typeof value === "number") { + if (!Number.isInteger(value) || value < 0) return null; + return BigInt(value); + } + if (typeof value === "string" && /^\d+$/.test(value)) return BigInt(value); + return null; +} + +function canonicalText(value: unknown) { + return String(value ?? "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +function parseReasonPath(rawPath: unknown) { + let category: string | null = null; + let detail: string | null = null; + + if (Array.isArray(rawPath)) { + const first = rawPath[0]; + const second = rawPath[1]; + if (typeof first === "string") category = first; + if (typeof second === "string") detail = second; + if (asRecord(first)) category = clampText(first.id ?? first.label ?? first.value, 120); + if (asRecord(second)) detail = clampText(second.id ?? second.label ?? second.value, 120); + } else if (typeof rawPath === "string") { + const pieces = rawPath + .split(/>|\/|\\|\|/g) + .map((p) => p.trim()) + .filter(Boolean); + category = pieces[0] ?? null; + detail = pieces[1] ?? null; + } + + return { + category: clampText(category, 120), + detail: clampText(detail, 120), + }; +} + +function parseReasonTextPath(reasonText: unknown) { + const text = clampText(reasonText, 240); + if (!text) return { category: null as string | null, detail: null as string | null }; + const pieces = text + .split(/>|\/|\\|\|/g) + .map((p) => p.trim()) + .filter(Boolean); + return { + category: clampText(pieces[0] ?? null, 120), + detail: clampText(pieces[1] ?? null, 120), + }; +} + +function findCatalogReasonFlexible( + catalog: ReasonCatalog | null, + kind: ReasonCatalogKind, + categoryIdOrLabel: unknown, + detailIdOrLabel: unknown +) { + const direct = findCatalogReason(catalog, kind, categoryIdOrLabel, detailIdOrLabel); + if (direct) return direct; + if (!catalog) return null; + + const catNeedle = canonicalText(categoryIdOrLabel); + const detNeedle = canonicalText(detailIdOrLabel); + if (!catNeedle || !detNeedle) return null; + + for (const category of catalog[kind] ?? []) { + const catMatch = + canonicalText(category.id) === catNeedle || canonicalText(category.label) === catNeedle; + if (!catMatch) continue; + for (const detail of category.details) { + const detMatch = canonicalText(detail.id) === detNeedle || canonicalText(detail.label) === detNeedle; + if (!detMatch) continue; + return { + categoryId: category.id, + categoryLabel: category.label, + detailId: detail.id, + detailLabel: detail.label, + reasonCode: toReasonCode(category.id, detail.id), + reasonLabel: `${category.label} > ${detail.label}`, + }; + } + } + return null; +} + +function getCatalogFromDefaults(defaultsJson: unknown) { + const defaults = asRecord(defaultsJson); + if (!defaults) return null; + return normalizeReasonCatalog(defaults.reasonCatalog ?? defaults.reasonCatalogData); +} + +function resolveReason( + raw: Record, + kind: ReasonCatalogKind, + catalog: ReasonCatalog | null, + fallbackVersion: number +) { + const reasonPath = parseReasonPath(raw.reasonPath); + const reasonTextPath = parseReasonTextPath(raw.reasonText); + const categoryIdRaw = clampText(raw.categoryId ?? reasonPath.category ?? reasonTextPath.category, 64); + const detailIdRaw = clampText(raw.detailId ?? reasonPath.detail ?? reasonTextPath.detail, 64); + const fromCatalog = findCatalogReasonFlexible(catalog, kind, categoryIdRaw, detailIdRaw); + + const categoryLabelRaw = clampText(raw.categoryLabel ?? reasonPath.category ?? reasonTextPath.category, 120); + const detailLabelRaw = clampText(raw.detailLabel ?? reasonPath.detail ?? reasonTextPath.detail, 120); + + const reasonCode = + clampText(raw.reasonCode, 64)?.toUpperCase() ?? + fromCatalog?.reasonCode ?? + toReasonCode(categoryIdRaw ?? categoryLabelRaw, detailIdRaw ?? detailLabelRaw) ?? + null; + + const categoryId = fromCatalog?.categoryId ?? categoryIdRaw; + const detailId = fromCatalog?.detailId ?? detailIdRaw; + const categoryLabel = fromCatalog?.categoryLabel ?? categoryLabelRaw; + const detailLabel = fromCatalog?.detailLabel ?? detailLabelRaw; + + const pathLabel = + clampText(raw.reasonText, 240) ?? + fromCatalog?.reasonLabel ?? + (categoryLabel && detailLabel ? `${categoryLabel} > ${detailLabel}` : null) ?? + detailLabel ?? + categoryLabel ?? + reasonCode; + + const catalogVersionRaw = numberFrom(raw.catalogVersion); + const catalogVersion = catalogVersionRaw != null ? Math.trunc(catalogVersionRaw) : fallbackVersion; + + return { + type: kind, + categoryId, + categoryLabel, + detailId, + detailLabel, + reasonCode, + reasonLabel: pathLabel, + reasonText: pathLabel, + catalogVersion, + }; +} + +export async function POST(req: Request) { + const apiKey = req.headers.get("x-api-key"); + if (!apiKey) return NextResponse.json({ ok: false, error: "Missing api key" }, { status: 401 }); + + let body: unknown = await req.json().catch(() => null); + + // ✅ if Node-RED sent an array as the whole body, unwrap it + if (Array.isArray(body)) body = body[0]; + const bodyRecord = asRecord(body) ?? {}; + const payloadRecord = asRecord(bodyRecord.payload) ?? {}; + + // ✅ accept multiple common keys + const machineId = + bodyRecord.machineId ?? + bodyRecord.machine_id ?? + (asRecord(bodyRecord.machine)?.id ?? null); + let rawEvent = + bodyRecord.event ?? + bodyRecord.events ?? + bodyRecord.anomalies ?? + payloadRecord.event ?? + payloadRecord.events ?? + payloadRecord.anomalies ?? + payloadRecord ?? + bodyRecord.data; // sometimes "data" + + const rawEventRecord = asRecord(rawEvent); + if (rawEventRecord?.event && typeof rawEventRecord.event === "object") rawEvent = rawEventRecord.event; + if (Array.isArray(rawEventRecord?.events)) rawEvent = rawEventRecord.events; + + if (!machineId || !rawEvent) { + return NextResponse.json( + { ok: false, error: "Invalid payload", got: { hasMachineId: !!machineId, keys: Object.keys(bodyRecord) } }, + { status: 400 } + ); + } + + if (!machineIdSchema.safeParse(String(machineId)).success) { + return NextResponse.json({ ok: false, error: "Invalid machine id" }, { status: 400 }); + } + + const machine = await getMachineAuth(String(machineId), apiKey); + if (!machine) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); + const orgSettings = await prisma.orgSettings.findUnique({ + where: { orgId: machine.orgId }, + select: { stoppageMultiplier: true, macroStoppageMultiplier: true, defaultsJson: true }, + }); + const fallbackCatalog = await loadFallbackReasonCatalog(); + const settingsCatalog = getCatalogFromDefaults(orgSettings?.defaultsJson); + const reasonCatalog = settingsCatalog ?? fallbackCatalog; + + const defaultMicroMultiplier = Number(orgSettings?.stoppageMultiplier ?? 1.5); + const defaultMacroMultiplier = Math.max( + defaultMicroMultiplier, + Number(orgSettings?.macroStoppageMultiplier ?? 5) + ); + + + // ✅ normalize to array no matter what + 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 skipped: Array> = []; + + for (const ev of events) { + const evRecord = asRecord(ev); + if (!evRecord) { + skipped.push({ reason: "invalid_event_object" }); + continue; + } + const evData = asRecord(evRecord.data) ?? {}; + // We'll re-check reason again after parsing `data` (it may be a JSON string) + let evReason = asRecord(evRecord.reason) ?? asRecord(evData.reason); + const evDowntime = asRecord(evRecord.downtime) ?? asRecord(evData.downtime); + // Some producers nest the reason under `downtime.reason` + if (!evReason) evReason = asRecord(evDowntime?.reason); + + const rawType = evRecord.eventType ?? evRecord.anomaly_type ?? evRecord.topic ?? bodyRecord.topic ?? ""; + const typ0 = normalizeType(rawType); + const typ = CANON_TYPE[typ0] ?? typ0; + + // Determine timestamp + const tsMs = + (typeof evRecord.timestamp === "number" && evRecord.timestamp) || + (typeof evData.timestamp === "number" && evData.timestamp) || + (typeof evData.event_timestamp === "number" && evData.event_timestamp) || + null; + + const ts = tsMs ? new Date(tsMs) : new Date(); + + // Severity defaulting (do not skip on severity — store for audit) + let sev = String(evRecord.severity ?? "").trim().toLowerCase(); + if (!sev) sev = "warning"; + + // Stop classification -> microstop/macrostop + let finalType = typ; + let stopSecForReason: number | null = null; + if (typ === "stop") { + const stopSec = + (typeof evData.stoppage_duration_seconds === "number" && evData.stoppage_duration_seconds) || + (typeof evData.stop_duration_seconds === "number" && evData.stop_duration_seconds) || + null; + stopSecForReason = stopSec != null ? Number(stopSec) : null; + + if (stopSec != null) { + const theoretical = Number(evData.theoretical_cycle_time ?? evData.theoreticalCycleTime ?? 0) || 0; + + const microMultiplier = Number( + evData.micro_threshold_multiplier ?? evData.threshold_multiplier ?? defaultMicroMultiplier + ); + const macroMultiplier = Math.max( + microMultiplier, + Number(evData.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 { + // missing duration -> conservative + finalType = "microstop"; + } + } + + if (!ALLOWED_TYPES.has(finalType)) { + skipped.push({ reason: "type_not_allowed", typ: finalType, sev }); + continue; + } + + const title = + clampText(evRecord.title, 160) || + (finalType === "slow-cycle" ? "Slow Cycle Detected" : + finalType === "macrostop" ? "Macrostop Detected" : + finalType === "microstop" ? "Microstop Detected" : + "Event"); + + const description = clampText(evRecord.description, 1000); + + // store full blob, ensure object + const rawData = evRecord.data ?? evRecord; + const parsedData = typeof rawData === "string" + ? (() => { + try { + return JSON.parse(rawData); + } catch { + return { raw: rawData }; + } + })() + : rawData; + const dataObj: Record = + parsedData && typeof parsedData === "object" && !Array.isArray(parsedData) + ? { ...(parsedData as Record) } + : { raw: parsedData }; + if (evRecord.status != null && dataObj.status == null) dataObj.status = evRecord.status; + if (evRecord.alert_id != null && dataObj.alert_id == null) dataObj.alert_id = evRecord.alert_id; + if (evRecord.is_update != null && dataObj.is_update == null) dataObj.is_update = evRecord.is_update; + if (evRecord.is_auto_ack != null && dataObj.is_auto_ack == null) dataObj.is_auto_ack = evRecord.is_auto_ack; + if (evReason && dataObj.reason == null) dataObj.reason = evReason; + if (evDowntime && dataObj.downtime == null) dataObj.downtime = evDowntime; + + // If `data` was a JSON string, the earlier evReason lookup would miss it. + // Re-check here using the normalized object we will persist. + if (!evReason) evReason = asRecord(dataObj.reason); + if (!evReason) evReason = asRecord(asRecord(dataObj.downtime)?.reason); + + // If we have a reasonText but missing ids, derive ids from the path-like string. + if (evReason) { + const hasCat = clampText((evReason as any).categoryId, 64) ?? clampText((evReason as any).categoryLabel, 120); + const hasDet = clampText((evReason as any).detailId, 64) ?? clampText((evReason as any).detailLabel, 120); + const rt = clampText((evReason as any).reasonText, 240); + if ((!hasCat || !hasDet) && rt) { + const parsed = parseReasonTextPath(rt); + const next = { ...evReason } as Record; + // Preserve any explicit ids; only fill gaps. + if ((next as any).categoryId == null && parsed.category) next.categoryId = canonicalText(parsed.category); + if ((next as any).categoryLabel == null && parsed.category) next.categoryLabel = parsed.category; + if ((next as any).detailId == null && parsed.detail) next.detailId = canonicalText(parsed.detail); + if ((next as any).detailLabel == null && parsed.detail) next.detailLabel = parsed.detail; + evReason = next; + } + } + + const activeWorkOrder = asRecord(evRecord.activeWorkOrder); + const dataActiveWorkOrder = asRecord(evData.activeWorkOrder); + + // ✨ Cada evento puede traer su propio seq, o usar el del payload raíz + const evSeq = + parseSeqToBigInt(evRecord.seq) ?? + parseSeqToBigInt(evData.seq) ?? + bodySeq; + + const evSchemaVersion = + clampText(evRecord.schemaVersion, 16) ?? + bodySchemaVersion; + + const eventData = { + orgId: machine.orgId, + machineId: machine.id, + schemaVersion: evSchemaVersion, + seq: evSeq, + ts, + topic: clampText(evRecord.topic ?? finalType, 64) ?? finalType, + eventType: finalType, + severity: sev, + requiresAck: !!evRecord.requires_ack, + title, + description, + data: toJsonValue(dataObj), + workOrderId: + clampText(evRecord.work_order_id, 64) ?? + clampText(evData.work_order_id, 64) ?? + clampText(activeWorkOrder?.id, 64) ?? + clampText(dataActiveWorkOrder?.id, 64) ?? + null, + sku: + clampText(evRecord.sku, 64) ?? + clampText(evData.sku, 64) ?? + clampText(activeWorkOrder?.sku, 64) ?? + clampText(dataActiveWorkOrder?.sku, 64) ?? + null, + }; + + // ✨ Idempotente: si ya existe (mismo orgId+machineId+seq), no inserta + const insertResult = await prisma.machineEvent.createMany({ + data: [eventData], + skipDuplicates: true, + }); + + // ✨ Buscar la fila (la recién creada o la duplicada existente) + let row; + if (evSeq != null) { + row = await prisma.machineEvent.findFirst({ + where: { + orgId: machine.orgId, + machineId: machine.id, + seq: evSeq, + }, + orderBy: { ts_server: "asc" }, + }); + } else { + // Sin seq, buscar por ts (fallback compatibilidad con eventos viejos) + row = await prisma.machineEvent.findFirst({ + where: { + orgId: machine.orgId, + machineId: machine.id, + ts, + eventType: finalType, + }, + orderBy: { ts_server: "desc" }, + }); + } + + if (!row) { + skipped.push({ reason: "row_not_found_after_insert", seq: evSeq?.toString() }); + continue; + } + + const wasDuplicate = insertResult.count === 0; + + // Si fue duplicado, no procesar reasonEntry ni alertas (ya se hicieron antes) + if (wasDuplicate) { + created.push({ id: row.id, ts: row.ts, eventType: row.eventType }); + continue; // ✨ saltar el resto del procesamiento + } + + created.push({ id: row.id, ts: row.ts, eventType: row.eventType }); + + + + // If the payload carries a `reason`, create the corresponding ReasonEntry. + // If it doesn't, still create an "UNCLASSIFIED" downtime ReasonEntry for stop events so the dashboard can show coverage. + if (evRecord.is_update || evRecord.is_auto_ack || dataObj.is_update || dataObj.is_auto_ack){ + // skip duplicate reasonEntry for refresh/ack + } else if (evReason || finalType === "microstop" || finalType === "macrostop" || finalType === "downtime-acknowledged" || finalType === "mold-change"){ + const moldIncidentKey = + clampText(evData.incidentKey ?? dataObj.incidentKey, 128) ?? + (numberFrom(evData.start_ms ?? dataObj.start_ms) != null + ? `mold-change:${Math.trunc(numberFrom(evData.start_ms ?? dataObj.start_ms) as number)}` + : null); + const reasonRaw: Record = + evReason ?? + (finalType === "mold-change" + ? ({ + type: "downtime", + categoryId: "cambio-molde", + detailId: "cambio-molde", + categoryLabel: "Cambio molde", + detailLabel: "Cambio molde", + reasonCode: "MOLD_CHANGE", + reasonText: "Cambio molde", + incidentKey: moldIncidentKey ?? row.id, + } as Record) + : + ({ + type: "downtime", + categoryId: "unclassified", + detailId: "unclassified", + categoryLabel: "Unclassified", + detailLabel: "Unclassified", + reasonCode: "UNCLASSIFIED", + reasonText: "Unclassified", + incidentKey: row.id, + } as Record)); + + const inferredKind: ReasonCatalogKind = + String(reasonRaw.type ?? "").toLowerCase() === "scrap" || finalType === "scrap-manual-entry" + ? "scrap" + : "downtime"; + const resolved = resolveReason(reasonRaw, inferredKind, reasonCatalog, reasonCatalog.version); + + if (resolved.reasonCode) { + const reasonId = + clampText(reasonRaw.reasonId, 128) ?? + (inferredKind === "downtime" + ? `evt:${machine.id}:downtime:${clampText((reasonRaw as any).incidentKey ?? evDowntime?.incidentKey, 128) ?? row.id}` + : `evt:${machine.id}:scrap:${clampText(reasonRaw.scrapEntryId, 128) ?? row.id}`); + + const workOrderId = + clampText(evRecord.work_order_id, 64) ?? + clampText(evData.work_order_id, 64) ?? + clampText(evRecord.workOrderId, 64) ?? + null; + + const commonWrite = { + reasonCode: resolved.reasonCode, + reasonLabel: resolved.reasonLabel ?? resolved.reasonCode, + reasonText: resolved.reasonText ?? null, + capturedAt: row.ts, + workOrderId, + schemaVersion: Math.max(1, Math.trunc(resolved.catalogVersion)), + meta: toJsonValue({ + source: "ingest:event", + eventId: row.id, + eventType: row.eventType, + incidentKey: clampText((reasonRaw as any).incidentKey ?? evDowntime?.incidentKey, 128), + anomalyType: + clampText(evRecord.anomalyType, 64) ?? + clampText(evDowntime?.anomalyType, 64) ?? + clampText(evRecord.anomaly_type, 64), + reason: { + type: resolved.type, + categoryId: resolved.categoryId, + categoryLabel: resolved.categoryLabel, + detailId: resolved.detailId, + detailLabel: resolved.detailLabel, + reasonText: resolved.reasonText, + catalogVersion: resolved.catalogVersion, + }, + }), + }; + + if (inferredKind === "downtime") { + const incidentKey = clampText((reasonRaw as any).incidentKey ?? evDowntime?.incidentKey, 128) ?? row.id; + const durationSeconds = + numberFrom(evDowntime?.durationSeconds) ?? + numberFrom(evData.duration_sec) ?? + numberFrom(evData.stoppage_duration_seconds) ?? + numberFrom(evData.stop_duration_seconds) ?? + (stopSecForReason != null ? stopSecForReason : null) ?? + null; + const episodeEndTsMs = + numberFrom(evData.end_ms) ?? + numberFrom(evDowntime?.episodeEndTsMs) ?? + numberFrom(evDowntime?.acknowledgedAtMs) ?? + null; + + await prisma.reasonEntry.upsert({ + where: { reasonId }, + create: { + orgId: machine.orgId, + machineId: machine.id, + reasonId, + kind: "downtime", + episodeId: incidentKey, + durationSeconds: durationSeconds != null ? Math.max(0, Math.trunc(durationSeconds)) : null, + episodeEndTs: episodeEndTsMs != null ? new Date(episodeEndTsMs) : null, + ...commonWrite, + }, + update: { + kind: "downtime", + episodeId: incidentKey, + durationSeconds: durationSeconds != null ? Math.max(0, Math.trunc(durationSeconds)) : null, + episodeEndTs: episodeEndTsMs != null ? new Date(episodeEndTsMs) : null, + ...commonWrite, + }, + }); + } else { + const scrapEntryId = + clampText((reasonRaw as any).scrapEntryId, 128) ?? + clampText(evRecord.id, 128) ?? + clampText(evRecord.eventId, 128) ?? + row.id; + const scrapQtyRaw = + numberFrom(evRecord.scrapDelta) ?? + numberFrom(evData.scrapDelta) ?? + numberFrom(evData.scrap_delta) ?? + 0; + const scrapQty = Math.max(0, Math.trunc(scrapQtyRaw)); + + await prisma.reasonEntry.upsert({ + where: { reasonId }, + create: { + orgId: machine.orgId, + machineId: machine.id, + reasonId, + kind: "scrap", + scrapEntryId, + scrapQty, + scrapUnit: clampText((reasonRaw as any).scrapUnit, 16) ?? null, + ...commonWrite, + }, + update: { + kind: "scrap", + scrapEntryId, + scrapQty, + scrapUnit: clampText((reasonRaw as any).scrapUnit, 16) ?? null, + ...commonWrite, + }, + }); + } + } + } + + try { + if (row.eventType !== "downtime-acknowledged" && row.eventType !== "scrap-manual-entry") { + await evaluateAlertsForEvent(row.id); + } + } catch (err) { + console.error("[alerts] evaluation failed", err); + } + } + + return NextResponse.json({ ok: true, createdCount: created.length, created, skippedCount: skipped.length, skipped }); +} diff --git a/app/api/ingest/heartbeat/route.ts b/app/api/ingest/heartbeat/route.ts index 3f1c4fd..cccce5e 100644 --- a/app/api/ingest/heartbeat/route.ts +++ b/app/api/ingest/heartbeat/route.ts @@ -97,26 +97,38 @@ export async function POST(req: Request) { // 5) Store heartbeat // Keep your legacy fields, but store meta fields too. const tsServerNow = new Date(); - const hb = await prisma.machineHeartbeat.create({ - data: { - orgId, - machineId: machine.id, + const hbRow = { + orgId, + machineId: machine.id, + schemaVersion, + seq, + ts: tsDeviceDate, + tsServer: tsServerNow, + status: body.status ? String(body.status) : (body.online ? "RUN" : "STOP"), + message: body.message ? String(body.message) : null, + ip: body.ip ? String(body.ip) : null, + fwVersion: body.fwVersion ? String(body.fwVersion) : null, + }; - // Phase 0 meta - schemaVersion, - seq, - ts: tsDeviceDate, - tsServer: tsServerNow, - - // Legacy payload compatibility - status: body.status ? String(body.status) : (body.online ? "RUN" : "STOP"), - message: body.message ? String(body.message) : null, - ip: body.ip ? String(body.ip) : null, - fwVersion: body.fwVersion ? String(body.fwVersion) : null, - }, + const insertHb = await prisma.machineHeartbeat.createMany({ + data: [hbRow], + skipDuplicates: true, }); - // Optional: update machine last seen (same as KPI) + const hb = await prisma.machineHeartbeat.findFirst({ + where: { + orgId, + machineId: machine.id, + ts: tsDeviceDate, + }, + orderBy: { tsServer: "asc" }, + }); + + if (!hb) { + return NextResponse.json({ ok: false, error: "Server error", detail: "Heartbeat row missing" }, { status: 500 }); + } + + // Optional: update machine last seen (same as KPI) — also on duplicate HB so lastSeen is fresh await prisma.machine.update({ where: { id: machine.id }, data: { @@ -132,6 +144,7 @@ export async function POST(req: Request) { id: hb.id, tsDevice: hb.ts, tsServer: hb.tsServer, + duplicate: insertHb.count === 0, }); } catch (err: unknown) { const msg = err instanceof Error ? err.message : "Unknown error"; diff --git a/app/api/ingest/kpi/route.ts b/app/api/ingest/kpi/route.ts index 245ad90..aebc638 100644 --- a/app/api/ingest/kpi/route.ts +++ b/app/api/ingest/kpi/route.ts @@ -222,44 +222,42 @@ export async function POST(req: Request) { : typeof woRecord.cavities === "number" && woRecord.cavities > 0 ? woRecord.cavities : null; - // Write snapshot (ts = tsDevice; tsServer auto) - const row = await prisma.machineKpiSnapshot.create({ - data: { - orgId, - machineId: machine.id, - - // Phase 0 meta - schemaVersion, - seq, - ts: tsDeviceDate, // store device-time in ts; server-time goes to ts_server - - // Work order fields - workOrderId: activeWorkOrderId || null, - sku: activeSku || null, - target: activeTargetQty, - good: good != null ? Math.trunc(good) : null, - scrap: scrap != null ? Math.trunc(scrap) : null, - - // Counters - cycleCount: snapshotCycleCount, - goodParts: snapshotGoodParts, - scrapParts: snapshotScrapParts, - cavities: safeCavities, - - // Cycle times - cycleTime: safeCycleTime, - actualCycle: typeof body.actualCycleTime === "number" ? body.actualCycleTime : null, - - // KPIs (0..100) - availability: typeof k.availability === "number" ? k.availability : null, - performance: typeof k.performance === "number" ? k.performance : null, - quality: typeof k.quality === "number" ? k.quality : null, - oee: typeof k.oee === "number" ? k.oee : null, - - trackingEnabled: typeof body.trackingEnabled === "boolean" ? body.trackingEnabled : null, - productionStarted: typeof body.productionStarted === "boolean" ? body.productionStarted : null, - }, + // Write snapshot (ts = tsDevice; tsServer auto). Idempotent on (org, machine, ts) to absorb retries. + const kpiData = { + orgId, + machineId: machine.id, + schemaVersion, + seq, + ts: tsDeviceDate, + workOrderId: activeWorkOrderId || null, + sku: activeSku || null, + target: activeTargetQty, + good: good != null ? Math.trunc(good) : null, + scrap: scrap != null ? Math.trunc(scrap) : null, + cycleCount: snapshotCycleCount, + goodParts: snapshotGoodParts, + scrapParts: snapshotScrapParts, + cavities: safeCavities, + cycleTime: safeCycleTime, + actualCycle: typeof body.actualCycleTime === "number" ? body.actualCycleTime : null, + availability: typeof k.availability === "number" ? k.availability : null, + performance: typeof k.performance === "number" ? k.performance : null, + quality: typeof k.quality === "number" ? k.quality : null, + oee: typeof k.oee === "number" ? k.oee : null, + trackingEnabled: typeof body.trackingEnabled === "boolean" ? body.trackingEnabled : null, + productionStarted: typeof body.productionStarted === "boolean" ? body.productionStarted : null, + }; + const insertKpi = await prisma.machineKpiSnapshot.createMany({ + data: [kpiData], + skipDuplicates: true, }); + const row = await prisma.machineKpiSnapshot.findFirst({ + where: { orgId, machineId: machine.id, ts: tsDeviceDate }, + orderBy: { tsServer: "asc" }, + }); + if (!row) { + return NextResponse.json({ ok: false, error: "Server error", detail: "KPI snapshot row missing" }, { status: 500 }); + } if (activeWorkOrderId) { await prisma.machineWorkOrder.upsert({ @@ -330,6 +328,7 @@ export async function POST(req: Request) { id: row.id, tsDevice: row.ts, tsServer: row.tsServer, + duplicate: insertKpi.count === 0, trace: traceEnabled ? trace : undefined, }); } catch (err: unknown) { diff --git a/components/recap/RecapMachineCard.tsx b/components/recap/RecapMachineCard.tsx index ae7f6a6..a0fbc14 100644 --- a/components/recap/RecapMachineCard.tsx +++ b/components/recap/RecapMachineCard.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { useEffect, useState } from "react"; import { useI18n } from "@/lib/i18n/useI18n"; +import { RECAP_HEARTBEAT_STALE_MS } from "@/lib/recap/recapUiConstants"; import type { RecapSummaryMachine, RecapTimelineResponse } from "@/lib/recap/types"; import RecapMiniTimeline from "@/components/recap/RecapMiniTimeline"; @@ -42,7 +43,8 @@ export default function RecapMachineCard({ machine, rangeStart, rangeEnd }: Prop const timelineStart = timeline?.range.start ?? rangeStart; const timelineEnd = timeline?.range.end ?? rangeEnd; const hasTimelineData = timeline?.hasData ?? timelineSegments.length > 0; - const staleHeartbeat = machine.lastSeenMs == null ? true : nowMs - machine.lastSeenMs > 5 * 60 * 1000; + const staleHeartbeat = + machine.lastSeenMs == null ? true : nowMs - machine.lastSeenMs > RECAP_HEARTBEAT_STALE_MS; const lastSeenLabel = machine.lastActivityMin == null diff --git a/components/recap/RecapProductionBySku.tsx b/components/recap/RecapProductionBySku.tsx index 2754091..b28e1a7 100644 --- a/components/recap/RecapProductionBySku.tsx +++ b/components/recap/RecapProductionBySku.tsx @@ -1,6 +1,7 @@ "use client"; import { useI18n } from "@/lib/i18n/useI18n"; +import { formatRecapProgressPercent } from "@/lib/recap/progressDisplay"; import type { RecapSkuRow } from "@/lib/recap/types"; type Props = { @@ -8,7 +9,7 @@ type Props = { }; export default function RecapProductionBySku({ rows }: Props) { - const { t } = useI18n(); + const { t, locale } = useI18n(); return (
@@ -30,7 +31,8 @@ export default function RecapProductionBySku({ rows }: Props) { {rows.slice(0, 10).map((row) => { - const progress = row.progressPct == null ? "--" : `${Math.round(row.progressPct)}%`; + const progress = + row.progressPct == null ? "—" : formatRecapProgressPercent(row.progressPct, locale); return ( {row.sku} diff --git a/components/recap/RecapWorkOrderStatus.tsx b/components/recap/RecapWorkOrderStatus.tsx index 99b64e0..0e510ac 100644 --- a/components/recap/RecapWorkOrderStatus.tsx +++ b/components/recap/RecapWorkOrderStatus.tsx @@ -1,6 +1,7 @@ "use client"; import { useI18n } from "@/lib/i18n/useI18n"; +import { formatRecapProgressPercent, progressBarWidthPercent } from "@/lib/recap/progressDisplay"; import type { RecapMachine } from "@/lib/recap/types"; type Props = { @@ -22,10 +23,13 @@ export default function RecapWorkOrderStatus({ workOrders }: Props) {
{workOrders.active.id}
SKU: {workOrders.active.sku || "--"}
+
+ {t("recap.production.progress")}: {formatRecapProgressPercent(workOrders.active.progressPct, locale)} +
diff --git a/components/recap/RecapWorkOrders.tsx b/components/recap/RecapWorkOrders.tsx index 2f65ec7..023320e 100644 --- a/components/recap/RecapWorkOrders.tsx +++ b/components/recap/RecapWorkOrders.tsx @@ -1,6 +1,7 @@ "use client"; import { useI18n } from "@/lib/i18n/useI18n"; +import { formatRecapProgressPercent, progressBarWidthPercent } from "@/lib/recap/progressDisplay"; import type { RecapWorkOrders as RecapWorkOrdersType } from "@/lib/recap/types"; type Props = { @@ -41,10 +42,14 @@ export default function RecapWorkOrders({ workOrders }: Props) {
{workOrders.active.id}
{t("recap.workOrders.sku")}: {workOrders.active.sku || "--"}
+
+ {t("recap.production.progress")}:{" "} + {formatRecapProgressPercent(workOrders.active.progressPct, locale)} +
diff --git a/flows (63).json b/flows (63).json index 7ee50f4..fca8d63 100644 --- a/flows (63).json +++ b/flows (63).json @@ -1 +1,3867 @@ -[{"id":"c19582f27bf28841","type":"subflow","name":"Outbox Enqueue v1 (1) (4) (2) (3) (1) (3)","info":"","category":"","in":[{"x":40,"y":40,"wires":[{"id":"b2bc325b50fc6722"}]}],"out":[{"x":1160,"y":40,"wires":[{"id":"f9f048e59b5d41a9","port":0}]}],"env":[],"meta":{},"color":"#DDAA99"},{"id":"b2bc325b50fc6722","type":"function","z":"c19582f27bf28841","name":"Prepare + Validate + Call next_seq","func":"// Outbox Enqueue v1 - Step 1\nconst config = global.get(\"config\") || {};\nconst out = msg.outbox || {};\nconst type = out.type;\nconst payload = out.payload;\n\nif (!type) throw new Error(\"Outbox Enqueue: missing msg.outbox.type\");\nif (!payload || typeof payload !== \"object\") throw new Error(\"Outbox Enqueue: missing/invalid msg.outbox.payload\");\n\n// Where to send later (Publisher will use this)\nconst endpointByType = {\n cycle: \"/api/ingest/cycle\",\n event: \"/api/ingest/event\",\n kpi: \"/api/ingest/kpi\",\n heartbeat: \"/api/ingest/heartbeat\",\n segment: \"/api/ingest/segment\", // future; ok to queue now even if not used yet\n};\n\nconst endpoint = out.endpoint || endpointByType[type];\nif (!endpoint) throw new Error(`Outbox Enqueue: unknown type '${type}' and no endpoint provided`);\n\n// Get machineId from msg or global config\nconst machineId = msg.machineId || config.machineId;\nif (!machineId) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Outbox waiting for pairing\" });\n return null;\n}\n\nconst schemaVersion = msg.schemaVersion || \"1.0\";\nconst tsMs = typeof msg.tsMs === \"number\" ? msg.tsMs : Date.now();\n\n// Stash meta for later nodes\nmsg._outboxMeta = { type, endpoint, machineId, schemaVersion, tsMs, payload };\n\nconst validators = {\n cycle: (p) => p && typeof p.cycle === \"object\",\n event: (p) => p && typeof p.event === \"object\",\n kpi: (p) => p && p.kpis && typeof p.kpis === \"object\",\n heartbeat: (p) => p && typeof p.status === \"string\",\n segment: (p) => p && typeof p === \"object\",\n};\n\nconst validator = validators[type];\nif (!validator || !validator(payload)) {\n node.warn(`Outbox Enqueue: invalid ${type} payload`);\n return null;\n}\n\n// Call stored procedure to get next seq (safe + persistent)\nmsg.topic = \"CALL next_seq(?);\";\nmsg.payload = [machineId];\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":220,"y":40,"wires":[["284de04f3e1dd8c7"]]},{"id":"284de04f3e1dd8c7","type":"mysql","z":"c19582f27bf28841","mydb":"fc9634aabefee16b","name":"CALL next_seq","x":460,"y":40,"wires":[["5a555283b4a45c58"]]},{"id":"5a555283b4a45c58","type":"function","z":"c19582f27bf28841","name":"Build envelope + prepare INSERT","func":"// Outbox Enqueue v1 - Step 2\nconst meta = msg._outboxMeta;\nif (!meta) throw new Error(\"Outbox Enqueue: missing _outboxMeta\");\n\nfunction stripNil(obj) {\n if (!obj || typeof obj !== \"object\") return obj;\n const out = Array.isArray(obj) ? [] : {};\n for (const [k, v] of Object.entries(obj)) {\n if (v === undefined || v === null) continue; // <— key change\n out[k] = (v && typeof v === \"object\") ? stripNil(v) : v;\n }\n return out;\n}\n\n// Parse seq from MySQL result\n// mysql node often returns an array of result sets for CALL\nlet seq = null;\nconst res = msg.payload;\n\nif (Array.isArray(res)) {\n // common shape: [ [ { seq: 123 } ], ... ]\n if (Array.isArray(res[0]) && res[0][0] && res[0][0].seq != null) seq = res[0][0].seq;\n // sometimes: [ { seq: 123 } ]\n if (seq == null && res[0] && res[0].seq != null) seq = res[0].seq;\n}\n\nif (seq == null) throw new Error(\"Outbox Enqueue: could not parse seq from DB result\");\n\n\n\n\nfunction normalizeIsoDeep(obj) {\n if (!obj || typeof obj !== \"object\") return obj;\n if (Array.isArray(obj)) return obj.map(normalizeIsoDeep);\n\n for (const k of Object.keys(obj)) {\n const v = obj[k];\n\n // recurse first\n if (v && typeof v === \"object\") obj[k] = normalizeIsoDeep(v);\n\n // ISO fields: must be string|null\n if (k.toLowerCase().endsWith(\"iso\")) {\n const vv = obj[k];\n if (typeof vv === \"string\") continue;\n if (typeof vv === \"number\") obj[k] = new Date(vv).toISOString();\n else if (vv == null) obj[k] = null;\n else obj[k] = null; // <- kills {} forever\n }\n }\n return obj;\n}\n\n\n\n\n\nconst cleanPayload = normalizeIsoDeep(stripNil(meta.payload));\nconst envelope = {\n schemaVersion: meta.schemaVersion,\n machineId: meta.machineId,\n tsMs: meta.tsMs,\n seq: String(seq),\n type: meta.type,\n payload: cleanPayload\n};\n\n\n// Prepare insert into outbox_messages\nmsg.topic = `\nINSERT INTO outbox_messages\n(machine_id, msg_type, endpoint, schema_version, seq, ts_device_ms, payload_json, status, attempts, next_attempt_at)\nVALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, NULL);\n`.trim();\n\nmsg.payload = [\n meta.machineId,\n meta.type,\n meta.endpoint,\n meta.schemaVersion,\n Number(seq), // BIGINT\n meta.tsMs, // BIGINT\n JSON.stringify(envelope),\n];\n\n// Keep a copy for debugging\nmsg._envelope = envelope;\nmsg._seq = Number(seq);\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":720,"y":40,"wires":[["f9f048e59b5d41a9"]]},{"id":"f9f048e59b5d41a9","type":"mysql","z":"c19582f27bf28841","mydb":"fc9634aabefee16b","name":"Insert outbox_messages","x":1010,"y":40,"wires":[[]]},{"id":"8ccf34b55a2afcad","type":"tab","label":"Flow 2.1","disabled":false,"info":"","env":[]},{"id":"c95c05b78d8464c5","type":"group","z":"8ccf34b55a2afcad","name":"Start ","style":{"stroke":"#92d04f","fill":"#addb7b","label":true},"nodes":["4319d292bf565c9c","d29b1f289eeaea5d","ac25819b6b50ab90","0319de4ecd45bdbf","cf7c59785d89fa2a","c72da0d5e173ccd5","fddb2e595128c225","6790210592277c61"],"x":1134,"y":139,"w":942,"h":142},{"id":"892a1a691044805e","type":"group","z":"8ccf34b55a2afcad","name":"Cavity Settings ","style":{"stroke":"#ff7f7f","fill":"#ffbfbf","label":true},"nodes":["d0779b8395bfabb2"],"x":122,"y":507,"w":926,"h":306},{"id":"472f828e204736a6","type":"group","z":"8ccf34b55a2afcad","name":"UI/UX","style":{"fill":"#d1d1d1","label":true},"nodes":["87dc48bbe753ab8d","2221910b5a895480","6feab0f06c780f4c","f4038d2505b262ba","372edd599e1b2fdd","405dccd3e09a99d4","6adb9fd7e98f4274","7de72e0542b98762","7b07faa72600f3fd","af1c33999012b1c1","c7a8bb7adab6838e","8f16fdca6d059017","47a6ea4de26fadce","de82c1e664a73bfb","a51caffa7492c805","59c4e2cd83fd1876","2139786b0733335d","5eeeac992826fa3b","3dd6baf608946fde","d7603f458aa33b0d","799fcf989e49f86a","5a1b8c2451faa879"],"x":214,"y":179,"w":672,"h":362},{"id":"1d1ce0cb54c52345","type":"group","z":"8ccf34b55a2afcad","name":"Work Orders","style":{"stroke":"#9363b7","fill":"#dbcbe7","label":true},"nodes":["0fe0cdc07941b31e","8fe1661536e76e10","65c3c2889a068c25","5df1ee6ee335a44a","8abddc40064f1f64","c48444f30fc71bae","e5272e3e630666ce","4a382caf6dadab4c","fc87eda3d68142da","8dcfbe64c30024e6","86abdbe356524296","07b66a930e3e5e40","ddf6d786b5a7e682","1e8c2370ba268726","35542f4dc31c3a96","1af33da0fece65a7","7cb1c8d40f510e0b","6f993b8cb99de446","2d893a89211ae18f","2872629594daf0ed","037e4825e2537ed2","16f0e85a7bc88164","a75b33fb4e9ebdf8","30be157b60fc55fd","a0a8bda316e17f8e","97399876ca829d1a","cb972412abfe909f","04accd50a0fe3c90","ca6e4da8d0c5f853","ea175b8208d85ed5","d116553e03c9a47c","1a14505b6501d2dc","286df6ee52e241ca","af0fb4f5a75407ac","c323fd9c710bcded","21e9ba4f889da77f","4bb96d61f9e3bba2","cc02262fda7af805"],"x":1034,"y":279,"w":1432,"h":682},{"id":"40295dcf4b5e1779","type":"group","z":"8ccf34b55a2afcad","name":"Anomaly System","style":{"stroke":"#ffC000","fill":"#ffdf7f","label":true},"nodes":["25e40f96b0876550","aa9948b7e5229b0d","2dffe9379e43af37","184c513e8d1389dd","d4ad56bfc7f333ba"],"x":224,"y":19,"w":922,"h":102},{"id":"1f2e45c551f40615","type":"group","z":"8ccf34b55a2afcad","name":"Alerts","style":{"fill":"#bfdbef","label":true},"nodes":["851a0e837a94ed78","6f2897368cb48370","154c7f1ad173419e"],"x":234,"y":819,"w":512,"h":82},{"id":"8b73333804b30ee0","type":"group","z":"8ccf34b55a2afcad","name":"Graphs","style":{"fill":"#bfbfbf","label":true},"nodes":["2848a91e7a88cd0b","52501193c1677944","429b86c710e30c33","89d2d406e3cd96ee","9d321f6ae2618e66"],"x":1194,"y":39,"w":802,"h":82},{"id":"d0779b8395bfabb2","type":"group","z":"8ccf34b55a2afcad","g":"892a1a691044805e","name":"Cavities Settings","style":{"stroke":"#ffff00","fill":"#ffffbf","label":true},"nodes":["c04e913f614d9036"],"x":148,"y":533,"w":874,"h":254},{"id":"c04e913f614d9036","type":"group","z":"8ccf34b55a2afcad","g":"d0779b8395bfabb2","name":"Settings","style":{"stroke":"#92d04f","fill":"#ffffbf","label":true},"nodes":["746bd5f6d88850e7","2c31593fa4f5fbaa","70860403340a5bc0","69540565a3ee537b","183adc7ca661960e","6b1a37faf14b1a55","f1a89a5092479152","b45e6bfabde1f79c"],"x":174,"y":559,"w":822,"h":202},{"id":"87dc48bbe753ab8d","type":"ui_template","z":"8ccf34b55a2afcad","g":"472f828e204736a6","group":"919b5b8d778e2b6c","name":"Home Template","order":0,"width":"25","height":"25","format":"\n
\n \n\n
\n
\n
\n
\n
OEE
\n
0%
\n
\n
\n
Disponibilidad
\n
0%
\n
\n
\n
Rendimiento
\n
0%
\n
\n
\n
Calidad
\n
0%
\n
\n
\n\n
\n

Orden de trabajo actual

\n
\n
\n
ID Orden trabajo
\n
 
\n
\n
\n
SKU
\n
 
\n
\n
\n
Tiempo de ciclo
\n
0
\n
\n
\n
\n
\n
0%
\n
\n
\n\n
\n
\n
Piezas buenas
\n
0
\n
de 0
\n
\n\n
\n
MaquinaOFFLINE
\n
ProducciónDETENIDA
\n
\n\n \n
\n Cambio de molde en curso desde {{ moldChange.startMs | date:'HH:mm' }} · {{ moldChangeElapsedMin() }} min\n
\n
\n \n
\n\n
\n
\n \n\n\n\n\n \n\n
\n
\n\n\n
\n
\n

Orden de trabajo en proceso

\n

{{ resumePrompt.id }}

\n

\n {{ resumePrompt.goodParts }} of {{ resumePrompt.targetQty }} partes completadas\n ({{ resumePrompt.progressPercent }}%)\n

\n

Ciclos: {{ resumePrompt.cycleCount }}

\n\n
\n \n \n
\n
\n
\n
\n
\n

{{ scrapPrompt.title || 'Orden de trabajo completada' }}

\n

{{ scrapPrompt.orderId }}

\n

\n Producido {{ scrapPrompt.produced }} de {{ scrapPrompt.target }} piezas\n

\n\n

\n Scrap acumulado: {{ scrapPrompt.scrapSoFar }}\n

\n\n
\n {{ scrapPrompt.manual ? '¿Cuántas piezas de scrap quieres agregar ahora?' : 'Hubo piezas de scrap?' }}\n
\n\n \n
\n
{{ scrapPrompt.scrapCount || 0 }}
\n
{{ scrapPrompt.error }}
\n\n
\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n
\n\n
\n \n
\n
\n\n \n
\n \n
\n \n \n
\n\n \n \n
\n
\n
\n\n
\n
\n

Selecciona razón de scrap

\n
\n Paso {{ scrapReasonPrompt.step }} de 2\n | {{ scrapReasonPrompt.selectedCategory.label }}\n
\n\n
\n \n
\n\n
\n \n
\n\n
\n \n \n
\n
\n
\n\n\n\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":380,"y":280,"wires":[["6adb9fd7e98f4274","c7a8bb7adab6838e","c3ef5e9b98a8e24f"]]},{"id":"2221910b5a895480","type":"ui_template","z":"8ccf34b55a2afcad","g":"472f828e204736a6","group":"e2f3a4b5c6d7e8f9","name":"Alerts Template","order":0,"width":"25","height":"25","format":"\n
\n \n\n
\n
\n
\n

Incidentes

\n
\n\n
\n \n \n \n
\n\n
\n
\n \n \n
\n
\n \n \n
\n \n
\n
\n
\n
\n\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":380,"y":360,"wires":[["6adb9fd7e98f4274","7a1de7abad9506ec"]]},{"id":"6feab0f06c780f4c","type":"ui_template","z":"8ccf34b55a2afcad","g":"472f828e204736a6","group":"e3f4a5b6c7d8e9f0","name":"Graphs Template","order":0,"width":"25","height":"25","format":"\n\n
\n \n\n
\n
\n

Graphs

\n\n\n
\n
\n

OEE

\n
\n
\n\n
\n

Disponibilidad

\n
\n
\n\n
\n

Rendimiendo

\n
\n
\n\n
\n

Calidad

\n
\n
\n
\n
\n
\n
\n\n\n\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":390,"y":400,"wires":[["6adb9fd7e98f4274"]]},{"id":"f4038d2505b262ba","type":"ui_template","z":"8ccf34b55a2afcad","g":"472f828e204736a6","group":"e4f5a6b7c8d9e0f1","name":"Help Template","order":0,"width":"25","height":"25","format":"\n
\n \n\n
\n
\n
\n

Help

\n
\n\n
\n

Acerca de este panel

\n

Esta interfaz monitorea los indicadores de Eficiencia Global del Equipo (OEE), el avance de producción y el registro de incidentes para operaciones de inyección de plástico. Navega entre las pestañas usando la barra lateral para acceder a órdenes de trabajo, monitoreo en tiempo real, gráficas de desempeño, registro de incidentes y configuración de máquina.

\n
\n\n
\n

Cómo empezar con una orden de trabajo

\n

Ve a la pestaña Órdenes de Trabajo y carga un archivo de Excel con tus órdenes, o selecciona una orden existente en la tabla. Haz clic en Cargar para activar una orden de trabajo y luego navega a la pestaña Inicio, donde verás los detalles de la orden actual. Antes de iniciar producción, configura tu molde en la pestaña Configuración, seleccionando un preset de molde o ingresando manualmente el número de cavidades.

\n
\n\n
\n

Ejecutando producción

\n

En la pestaña Inicio, presiona el botón INICIAR para comenzar la producción. El sistema registra conteo de ciclos, piezas buenas, scrap y el avance hacia tu cantidad objetivo. Presiona DETENER para pausar la producción. Monitorea en tiempo real los KPIs, incluyendo OEE, Disponibilidad, Rendimiento y Calidad mostrados en el tablero.

\n
\n\n
\n

Registro de incidentes

\n

Usa la pestaña Alertas para registrar incidentes de producción. Registra rápidamente problemas comunes con botones preconfigurados como Falta de Material, Máquina Detenida o Paro de Emergencia. Para un registro más detallado, selecciona un tipo de alerta en el menú desplegable, agrega notas y envía. Todos los incidentes se registran con sello de tiempo y se utilizan en los cálculos de Disponibilidad y OEE.

\n
\n\n
\n

Configuración de moldes

\n

En la pestaña Configuración, utiliza la sección de Preconfigurados de Molde para buscar tu molde por fabricante y nombre. Selecciona una configuración para cargar automáticamente el número de cavidades, o ajusta manualmente los campos de Configuración de Molde. Si tu molde no aparece, usa el botón Agregar Molde en la sección de Integraciones para crear un nuevo preset con fabricante, nombre y detalles de cavidades.

\n
\n\n
\n

Visualización de datos de desempeño

\n

La pestaña Gráficas muestra el historial de tendencias de OEE desglosado por Disponibilidad, Desempeño y Calidad. Usa estas gráficas para identificar patrones, dar seguimiento a mejoras y diagnosticar problemas recurrentes que afecten la eficiencia de tu producción

\n
\n
\n
\n
\n\n\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":380,"y":440,"wires":[["6adb9fd7e98f4274"]]},{"id":"372edd599e1b2fdd","type":"ui_template","z":"8ccf34b55a2afcad","g":"472f828e204736a6","group":"e5f6a7b8c9d0e1f2","name":"Settings Template","order":0,"width":"25","height":"25","format":"\n
\n \n\n
\n
\n
\n

Ajustes

\n
\n\n
\n

Moldes Preconfigurados

\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n
\n \n
\n

Seleccionar un fabricante y el molde de las siguientes opciones.

\n
\n\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n \n \n
\n
\n
\n
\n\n
\n

Configuraciones de Molde

\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n\n
\n

Integraciones

\n
\n
\n \n
\n
\n

No encuentras lo que buscas?

\n \n
\n
\n
\n
\n

Enlace Control Tower

\n

Ingresa el codigo de 5 caracteres que aparece en Control Tower para enlazar esta Raspberry Pi.

\n
\n
\n \n \n
\n
\n \n
\n
\n

{{ pairingStatus }}

\n
\n
\n

WiFi

\n

Conecta esta Raspberry Pi a una red WiFi (sin salir del dashboard).

\n \n
\n
\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n \n \n
\n \n
\n \n \n
\n
\n \n
\n \n \n \n \n \n
\n \n

\n {{ wifiStatusText }}\n

\n
\n
\n

Programación de Producción

\n\n \n
\n
\n \n
\n
\n\n
\n
\n \n \n
\n
\n \n \n
\n
1\">\n \n
\n
\n\n \n\n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n \n \n {{ saveStatus }}\n \n
\n\n
\n

Umbral OEE

\n
\n
\n \n \n

\n Gap > (Cycle Time × {{thresholdMultiplier || 1.5}}) = Stoppage\n

\n
\n
\n \n \n
\n
\n
\n
\n
\n
\n\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":390,"y":480,"wires":[["6adb9fd7e98f4274","af1c33999012b1c1","3dd6baf608946fde"]]},{"id":"405dccd3e09a99d4","type":"ui_template","z":"8ccf34b55a2afcad","g":"472f828e204736a6","group":"b99c269687d574aa","name":"WO Template","order":1,"width":"25","height":"25","format":"\n
\n \n\n
\n
\n
\n

Ordenes de trabajo

\n
\n \n 0 selected\n \n \n \n \n \n \n \n
\n
\n\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
IDSKUMETABUENASSCRAPPROGRESOSTATUSULTIMA ACTUALIZACIÓN
\n
\n
\n 0 items\n Tip: Dejale picado para seleccionar multiples ordenes\n
\n
\n
\n
\n
\n\n\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":380,"y":320,"wires":[["6adb9fd7e98f4274","c7a8bb7adab6838e"]]},{"id":"6adb9fd7e98f4274","type":"function","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"Tab navigation","func":"if (msg.ui_control && msg.ui_control.tab) {\n msg.payload = { tab: msg.ui_control.tab };\n delete msg.ui_control;\n return msg;\n}\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":380,"wires":[["7de72e0542b98762"]]},{"id":"7de72e0542b98762","type":"ui_ui_control","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"ui_control","events":"all","x":800,"y":380,"wires":[[]]},{"id":"7b07faa72600f3fd","type":"ui_template","z":"8ccf34b55a2afcad","g":"472f828e204736a6","group":"","name":"General Style","order":0,"width":0,"height":0,"format":"","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"global","className":"","x":760,"y":300,"wires":[[]]},{"id":"25e40f96b0876550","type":"ui_template","z":"8ccf34b55a2afcad","g":"40295dcf4b5e1779","group":"919b5b8d778e2b6c","name":"Anomaly Alert System (Global)","order":1,"width":"25","height":"25","format":"\n\n\n\n\n\n
\n
\n

Active Alerts

\n \n
\n\n
\n
\n
\n

No active alerts

\n

All systems operating normally

\n
\n\n
\n
\n

{{anomaly.title}}

\n {{anomaly.severity}}\n
\n

{{anomaly.description}}

\n

{{formatTimestamp(anomaly.tsMs)}}

\n
\n \n
\n
\n
\n
\n\n\n
\n
\n
\n

{{popup.title}}

\n \n
\n

{{popup.description}}

\n \n
\n
\n\n
\n
\n

Selecciona razón de paro

\n

{{reasonPrompt.anomalyTitle || 'Incidente de downtime'}}

\n
\n Paso {{reasonPrompt.step}} de 2\n | {{reasonPrompt.selectedCategory.label}}\n
\n\n
\n \n
\n\n
\n \n
\n\n
\n \n \n
\n
\n
\n\n\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":430,"y":60,"wires":[["aa9948b7e5229b0d","7bd006e12583f8e1"]]},{"id":"aa9948b7e5229b0d","type":"function","z":"8ccf34b55a2afcad","g":"40295dcf4b5e1779","name":"Handle Anomaly Acknowledgment","func":"// Handle anomaly acknowledgments and downtime-reason submit.\n// Output 1: SQL update for anomaly_events\n// Output 2: event payload for Build Event Outbox Payload\nconst anomaly = global.get(\"anomaly\") || {};\nconst topic = msg.topic || \"\";\n\nfunction persistAnomaly(next) {\n global.set(\"anomaly\", next);\n try {\n global.set(\"anomaly\", next, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n}\n\nfunction parseReason(payload) {\n const reasonPath = Array.isArray(payload.reasonPath) ? payload.reasonPath : [];\n const category = reasonPath[0] || {};\n const detail = reasonPath[1] || {};\n return {\n type: payload.reasonType || \"downtime\",\n categoryId: category.id || null,\n categoryLabel: category.label || null,\n detailId: detail.id || null,\n detailLabel: detail.label || null,\n reasonText: payload.reasonText || [\n category.label || \"\",\n detail.label || \"\"\n ].filter(Boolean).join(\" > \") || null,\n catalogVersion: payload.catalogVersion || null,\n incidentKey: payload.incidentKey || payload.incident_key || null\n };\n}\n\nfunction removeFromActive(eventId) {\n let activeAnomalies = anomaly.activeAnomalies || [];\n const idx = activeAnomalies.findIndex((a) => a.event_id === eventId || a.tsMs === eventId);\n if (idx !== -1) {\n activeAnomalies.splice(idx, 1);\n anomaly.activeAnomalies = activeAnomalies;\n }\n}\n\nif (topic === \"acknowledge-anomaly\" || topic === \"anomaly-reason-submit\") {\n const ackData = msg.payload || {};\n const eventId = ackData.event_id ?? ackData.tsMs;\n const ackTimestamp = typeof ackData.tsMs === \"number\" ? ackData.tsMs : Date.now();\n const incidentKey = ackData.incidentKey || ackData.incident_key || null;\n\n if (!eventId) {\n node.warn(\"[ANOMALY ACK] No event_id provided\");\n return null;\n }\n\n if (String(eventId).startsWith(\"temp_\")) {\n removeFromActive(eventId);\n persistAnomaly(anomaly);\n return null;\n }\n\n const sql = (\n \"UPDATE anomaly_events \" +\n \"SET status = 'acknowledged', acknowledged_at = \" + Number(ackTimestamp) + \" \" +\n \"WHERE event_timestamp = \" + Number(eventId)\n );\n const dbMsg = { topic: sql, payload: [] };\n\n removeFromActive(eventId);\n\n if (topic === \"anomaly-reason-submit\") {\n anomaly.reasonsByIncident = anomaly.reasonsByIncident || {};\n anomaly.ackedIncidentKeys = anomaly.ackedIncidentKeys || {};\n\n const reason = parseReason(ackData);\n if (incidentKey) {\n anomaly.reasonsByIncident[incidentKey] = reason;\n anomaly.ackedIncidentKeys[incidentKey] = true;\n }\n\n persistAnomaly(anomaly);\n\n const alreadySent = !!(incidentKey && anomaly.reasonEventsSent && anomaly.reasonEventsSent[incidentKey]);\n if (alreadySent) {\n return [dbMsg, null];\n }\n\n if (incidentKey) {\n anomaly.reasonEventsSent = anomaly.reasonEventsSent || {};\n anomaly.reasonEventsSent[incidentKey] = true;\n persistAnomaly(anomaly);\n }\n\n const eventTsMs = Number(ackData.tsMs) || Date.now();\n const anomalyType = ackData.anomalyType || ackData.anomaly_type || \"downtime\";\n const outEvent = {\n tsMs: eventTsMs,\n eventType: \"downtime-acknowledged\",\n anomalyType,\n eventId: eventId,\n incidentKey: incidentKey || null,\n // Keep reason attached to generic event payload for current ingest.\n reason,\n // Explicit separation for future split in Control Tower.\n downtime: {\n incidentKey: incidentKey || null,\n eventId: eventId,\n anomalyType,\n acknowledgedAtMs: eventTsMs,\n reason\n }\n };\n\n return [{ ...dbMsg }, { payload: outEvent, tsMs: eventTsMs }];\n }\n\n if (incidentKey) {\n anomaly.ackedIncidentKeys = anomaly.ackedIncidentKeys || {};\n anomaly.ackedIncidentKeys[incidentKey] = true;\n }\n persistAnomaly(anomaly);\n\n return [dbMsg, null];\n}\n\nreturn null;\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":720,"y":60,"wires":[["2dffe9379e43af37"],["a1e59ef54a91ee50"]]},{"id":"4319d292bf565c9c","type":"inject","z":"8ccf34b55a2afcad","g":"c95c05b78d8464c5","name":"Simula Inyectora","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":1260,"y":180,"wires":[["c72da0d5e173ccd5","e8efa837400e163b","aa698e63d1f67845","6790210592277c61"]]},{"id":"d29b1f289eeaea5d","type":"function","z":"8ccf34b55a2afcad","g":"c95c05b78d8464c5","name":"1,0","func":"// Get current global value (default to 0 if not set)\nconst state = global.get(\"state\") || {};\nlet estado = state.machineToggle || 0;\nlet stop = flow.get('stop') || false;\n\nif (stop) {\n // Manual stop active → force 0, don't reschedule\n state.machineToggle = 0;\nglobal.set(\"state\", state);\n msg.payload = 0;\n node.send(msg);\n return;\n}\n\n// Toggle between 1 and 0\nestado = estado === 1 ? 0 : 1;\n\n// Update the global variable\nstate.machineToggle = estado;\nglobal.set(\"state\", state);\n\n// Send it out\nmsg.payload = estado;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1670,"y":200,"wires":[["ac25819b6b50ab90"]]},{"id":"746bd5f6d88850e7","type":"function","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","name":"Cavities Settings","func":"const settings = global.get(\"settings\") || {};\nconst state = global.get(\"state\") || {};\nconst moldByWorkOrder = state.moldByWorkOrder || {};\n\nconst persistSettings = () => {\n global.set(\"settings\", settings);\n try {\n global.set(\"settings\", settings, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst persistMoldCache = () => {\n const cache = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n };\n global.set(\"moldCache\", cache);\n try {\n global.set(\"moldCache\", cache, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst persistState = () => {\n global.set(\"state\", state);\n};\n\nconst persistMoldSelection = (total, active) => {\n if (!Number.isFinite(active) || active <= 0) return;\n state.lastMoldActive = active;\n if (Number.isFinite(total) && total > 0) {\n state.lastMoldTotal = total;\n }\n if (state.activeWorkOrder?.id) {\n state.activeWorkOrder.cavities = active;\n if (Number.isFinite(total) && total > 0) {\n state.activeWorkOrder.cavities_total = total;\n }\n moldByWorkOrder[state.activeWorkOrder.id] = {\n total: Number.isFinite(total) && total > 0 ? total : (state.lastMoldTotal ?? null),\n active\n };\n }\n state.moldByWorkOrder = moldByWorkOrder;\n persistState();\n persistMoldCache();\n};\n\nconst buildCavitiesUpdate = (total, active) => {\n const activeId = state.activeWorkOrder?.id;\n if (!activeId || !Number.isFinite(active) || active <= 0) return null;\n const totalSafe = Number.isFinite(total) && total > 0 ? total : 0;\n return {\n topic: \"UPDATE work_orders SET cavities_total = COALESCE(NULLIF(?,0), cavities_total), cavities_active = COALESCE(NULLIF(?,0), cavities_active), updated_at = NOW() WHERE work_order_id = ?\",\n payload: [totalSafe, active, activeId]\n };\n};\n\nif (msg.topic === \"moldSettings\" && msg.payload) {\n const total = Number(msg.payload.total || 0);\n const active = Number(msg.payload.active || 0);\n\n // Store settings\n settings.moldTotal = total;\n settings.moldActive = active;\n persistSettings();\n\n persistMoldSelection(total, active);\n\n node.status({ fill: \"green\", shape: \"dot\", text: `Saved: ${active}/${total}` });\n\n const dbMsg = buildCavitiesUpdate(total, active);\n\n msg.payload = { saved: true, total, active };\n return [msg, dbMsg];\n}\n\n// Handle preset selection\nif (msg.topic === \"selectMoldPreset\" && msg.payload) {\n const preset = msg.payload;\n const total = Number(preset.theoretical_cavities || 0);\n const active = Number(preset.functional_cavities || 0);\n\n // Store settings\n settings.moldTotal = total;\n settings.moldActive = active;\n persistSettings();\n\n persistMoldSelection(total, active);\n\n node.status({ fill: \"blue\", shape: \"dot\", text: `Preset: ${preset.mold_name}` });\n\n // Send to UI to update fields\n msg.topic = \"moldPresetSelected\";\n msg.payload = { total, active, presetName: preset.mold_name };\n\n const dbMsg = buildCavitiesUpdate(total, active);\n\n return [msg, dbMsg];\n}\n\n//node.status({ fill: \"red\", shape: \"ring\", text: \"Invalid payload\" });\nreturn [null, null];","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":600,"wires":[["69540565a3ee537b"]]},{"id":"2c31593fa4f5fbaa","type":"function","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","name":"Mold Presets Handler","func":"const topic = msg.topic || '';\nconst payload = msg.payload || {};\n\n// ===== IGNORE NON-MOLD TOPICS SILENTLY =====\n// These are KPI/dashboard messages not meant for this handler\nconst ignoredTopics = [\n 'machineStatus',\n 'kpis',\n 'chartsData',\n 'activeWorkOrder',\n 'workOrderCycle',\n 'workOrdersList',\n 'scrapPrompt',\n 'uploadStatus'\n];\n\nif (ignoredTopics.includes(topic) || topic === '') {\n return null; // Silent ignore\n}\n\n// Log only mold-related requests\nnode.warn(`Received: ${topic}`);\n\n// CRITICAL: Use a processing lock to prevent simultaneous requests\nlet dedupeKey = topic;\nif (topic === 'addMoldPreset') {\n dedupeKey = `add_${payload.manufacturer}_${payload.mold_name}`;\n} else if (topic === 'getMoldsByManufacturer') {\n dedupeKey = `getmolds_${payload.manufacturer}`;\n}\n\nconst lockKey = `lock_${dedupeKey}`;\nconst lastRequestKey = `last_request_${dedupeKey}`;\n\n// Check if currently processing this request\nif (flow.get(lockKey) === true) {\n node.warn(`${topic} already processing - duplicate blocked`);\n return null;\n}\n\n// Check timing\nconst now = Date.now();\nconst lastRequestTime = flow.get(lastRequestKey) || 0;\nif (now - lastRequestTime < 2000) {\n node.warn(`Duplicate ${topic} request ignored (within 2s)`);\n return null;\n}\n\n// Set lock IMMEDIATELY before any async operations\nflow.set(lockKey, true);\nflow.set(lastRequestKey, now);\n\n// Release lock after 3 seconds (safety timeout)\nsetTimeout(() => {\n flow.set(lockKey, false);\n}, 3000);\n\n// Load all presets (legacy)\nif (topic === 'loadMoldPresets') {\n msg._originalTopic = 'loadMoldPresets';\n msg.topic = 'SELECT * FROM mold_presets ORDER BY manufacturer, mold_name;';\n node.warn('Querying all presets');\n return msg;\n}\n\n// Search/filter presets (legacy)\nif (topic === 'searchMoldPresets') {\n const filters = msg.payload || {};\n const searchTerm = (filters.searchTerm || '').trim().replace(/['\\\"\\\\\\\\]/g, '');\n const manufacturer = (filters.manufacturer || '').replace(/['\\\"\\\\\\\\]/g, '');\n const theoreticalCavities = filters.theoreticalCavities || '';\n\n let query = 'SELECT * FROM mold_presets WHERE 1=1';\n\n if (searchTerm) {\n const searchPattern = `%${searchTerm}%`;\n query += ` AND (mold_name LIKE '${searchPattern.replace(/'/g, \"''\")}' OR manufacturer LIKE '${searchPattern.replace(/'/g, \"''\")}')`;\n }\n\n if (manufacturer && manufacturer !== 'All') {\n query += ` AND manufacturer = '${manufacturer.replace(/'/g, \"''\")}'`;\n }\n\n if (theoreticalCavities && theoreticalCavities !== '') {\n const cavities = Number(theoreticalCavities);\n if (!isNaN(cavities)) {\n query += ` AND theoretical_cavities = ${cavities}`;\n }\n }\n\n query += ' ORDER BY manufacturer, mold_name;';\n\n msg._originalTopic = 'searchMoldPresets';\n msg.topic = query;\n return msg;\n}\n\n// Get unique manufacturers for dropdown\nif (topic === 'getManufacturers') {\n msg._originalTopic = 'getManufacturers';\n msg.topic = 'SELECT DISTINCT manufacturer FROM mold_presets ORDER BY manufacturer;';\n node.warn('Querying manufacturers');\n return msg;\n}\n\n// Get molds for a specific manufacturer\nif (topic === 'getMoldsByManufacturer') {\n const data = msg.payload || {};\n const manufacturerRaw = (data.manufacturer || '').trim();\n if (!manufacturerRaw) {\n node.warn('No manufacturer provided');\n return null;\n }\n\n const manufacturerSafe = manufacturerRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n\n msg._originalTopic = 'getMoldsByManufacturer';\n msg.topic = `SELECT * FROM mold_presets WHERE manufacturer = '${manufacturerSafe}' ORDER BY mold_name;`;\n node.warn(`Querying molds for: ${manufacturerSafe}`);\n return msg;\n}\n\n// Add a new mold preset - CRITICAL: Strong deduplication\nif (topic === 'addMoldPreset') {\n const data = msg.payload || {};\n const manufacturerRaw = (data.manufacturer || '').trim();\n const moldNameRaw = (data.mold_name || '').trim();\n const theoreticalRaw = (data.theoretical || '').trim();\n const activeRaw = (data.active || '').trim();\n\n if (!manufacturerRaw || !moldNameRaw || !theoreticalRaw || !activeRaw) {\n node.status({ fill: 'red', shape: 'ring', text: 'Missing value' });\n node.warn('Missing required fields');\n return null;\n }\n\n // Additional safety check for already-processed flag\n if (msg._addMoldProcessed) {\n node.warn('addMoldPreset already processed flag detected, ignoring');\n return null;\n }\n msg._addMoldProcessed = true;\n\n const manufacturerSafe = manufacturerRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n const moldNameSafe = moldNameRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n const theoreticalSafe = theoreticalRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n const activeSafe = activeRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n\n msg._originalTopic = 'addMoldPreset';\n msg.topic =\n \"INSERT INTO mold_presets (manufacturer, mold_name, theoretical_cavities, functional_cavities) \" +\n \"VALUES ('\" + manufacturerSafe + \"', '\" + moldNameSafe + \"', \" + theoreticalSafe + \", \" + activeSafe + \");\";\n\n node.status({ fill: 'blue', shape: 'dot', text: 'Inserting mold...' });\n node.warn(`Inserting: ${manufacturerSafe} - ${moldNameSafe}`);\n return msg;\n}\n\nnode.warn(`Unknown topic: ${topic}`);\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":660,"wires":[["70860403340a5bc0"]]},{"id":"70860403340a5bc0","type":"mysql","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","mydb":"fc9634aabefee16b","name":"Mold Presets DB","x":610,"y":660,"wires":[["69540565a3ee537b"]]},{"id":"69540565a3ee537b","type":"function","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","name":"Process DB Results","func":"// Replace function in \"Process DB Results\" node\n\nconst originalTopic = msg._originalTopic || '';\nconst dbResults = Array.isArray(msg.payload) ? msg.payload : [];\n\nif (!originalTopic) {\n return null;\n}\n\n// IMPORTANT: Clear socketid to prevent loops back to sender\ndelete msg._socketid;\ndelete msg.socketid;\n\n// Manufacturers query → list for first dropdown\nif (originalTopic === 'getManufacturers') {\n const manufacturers = dbResults\n .map(row => row.manufacturer)\n .filter((mfg, index, arr) => mfg && arr.indexOf(mfg) === index)\n .sort();\n\n msg.topic = 'manufacturersList';\n msg.payload = manufacturers;\n\n node.status({ fill: 'green', shape: 'dot', text: `${manufacturers.length} manufacturers` });\n return msg;\n}\n\n// Preset lists (legacy load/search)\nif (originalTopic === 'loadMoldPresets' || originalTopic === 'searchMoldPresets') {\n const presets = dbResults.map(row => ({\n mold_name: row.mold_name || '',\n manufacturer: row.manufacturer || '',\n theoretical_cavities: Number(row.theoretical_cavities) || 0,\n functional_cavities: Number(row.functional_cavities) || 0\n }));\n\n msg.topic = 'moldPresetsList';\n msg.payload = presets;\n\n node.status({ fill: 'green', shape: 'dot', text: `${presets.length} presets found` });\n return msg;\n}\n\n// Molds for selected manufacturer\nif (originalTopic === 'getMoldsByManufacturer') {\n const presets = dbResults.map(row => ({\n mold_name: row.mold_name || '',\n manufacturer: row.manufacturer || '',\n theoretical_cavities: Number(row.theoretical_cavities) || 0,\n functional_cavities: Number(row.functional_cavities) || 0\n }));\n\n msg.topic = 'moldPresetsList';\n msg.payload = presets;\n\n node.status({ fill: 'blue', shape: 'dot', text: `${presets.length} molds for manufacturer` });\n return msg;\n}\n\n// Result of inserting a new mold\nif (originalTopic === 'addMoldPreset') {\n msg.topic = 'addMoldResult';\n msg.payload = {\n success: true,\n result: msg.payload\n };\n\n node.status({ fill: 'green', shape: 'dot', text: 'Mold added' });\n return msg;\n}\n\nnode.status({ fill: 'yellow', shape: 'ring', text: 'Unknown topic: ' + originalTopic });\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":740,"y":600,"wires":[["6b1a37faf14b1a55"]]},{"id":"af1c33999012b1c1","type":"link out","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"link out 1","mode":"link","links":["183adc7ca661960e"],"x":505,"y":480,"wires":[]},{"id":"183adc7ca661960e","type":"link in","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","name":"link in 1","links":["af1c33999012b1c1"],"x":215,"y":660,"wires":[["746bd5f6d88850e7","2c31593fa4f5fbaa","f1a89a5092479152","c904233ea449e9e0"]]},{"id":"1af33da0fece65a7","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Work Order buttons","func":"const config = global.get(\"config\") || {};\nconst settings = global.get(\"settings\") || {};\nconst state = global.get(\"state\") || {};\nconst reasonCatalog = settings.reasonCatalog || {};\n\nconst persistMoldCache = () => {\n const cache = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n };\n global.set(\"moldCache\", cache);\n try {\n global.set(\"moldCache\", cache, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst moldByWorkOrder = state.moldByWorkOrder || {};\nconst attachMold = (order) => {\n if (!order || !order.id) return;\n const mapped = moldByWorkOrder[order.id] || {};\n const cavities = Number(order.cavities ?? mapped.active ?? state.lastMoldActive ?? 0);\n if (!Number.isFinite(cavities) || cavities <= 0) return;\n const total = Number(order.cavities_total ?? order.cavitiesTotal ?? mapped.total ?? state.lastMoldTotal ?? settings.moldTotal ?? 0);\n order.cavities = cavities;\n if (Number.isFinite(total) && total > 0) {\n order.cavities_total = total;\n state.lastMoldTotal = total;\n }\n state.lastMoldActive = cavities;\n moldByWorkOrder[order.id] = {\n total: Number.isFinite(total) && total > 0 ? total : (state.lastMoldTotal ?? null),\n active: cavities\n };\n state.moldByWorkOrder = moldByWorkOrder;\n};\n\nconst finalize = (ret) => {\n global.set(\"state\", state);\n persistMoldCache();\n return ret;\n};\n\nconst normalizeReason = (payload, fallbackType) => {\n const path = Array.isArray(payload.reasonPath) ? payload.reasonPath : [];\n const category = path[0] || {};\n const detail = path[1] || {};\n return {\n type: payload.reasonType || fallbackType || \"downtime\",\n categoryId: category.id || null,\n categoryLabel: category.label || null,\n detailId: detail.id || null,\n detailLabel: detail.label || null,\n reasonText: payload.reasonText || [\n category.label || \"\",\n detail.label || \"\"\n ].filter(Boolean).join(\" > \") || null,\n catalogVersion: payload.catalogVersion || reasonCatalog.version || null,\n incidentKey: payload.incidentKey || null\n };\n};\n\n\n// ===== MOLD CHANGE AUTO-CLOSE =====\n// If a mold change is active and user is starting/resuming/restarting a WO, close it.\nif (state.moldChange && state.moldChange.active &&\n (msg.action === \"start\" || msg.action === \"start-work-order\" || msg.action === \"resume-work-order\" || msg.action === \"restart-work-order\")) {\n const now = Date.now();\n const mc = state.moldChange;\n const durationSec = Math.max(0, Math.round((now - (mc.startMs || now)) / 1000));\n const closedMc = {\n active: false,\n startMs: mc.startMs,\n endMs: now,\n fromMoldId: mc.fromMoldId || null,\n toMoldId: state.lastMoldActive || null,\n durationSec\n };\n state.moldChange = closedMc;\n global.set(\"state\", state);\n const closeEvent = {\n payload: {\n anomaly_type: \"mold-change\",\n eventType: \"mold-change\",\n severity: \"info\",\n requires_ack: false,\n title: \"Cambio de molde finalizado (automatico)\",\n description: \"Duracion: \" + Math.round(durationSec/60) + \" min\",\n status: \"resolved\",\n tsMs: now,\n data: {\n status: \"resolved\",\n start_ms: closedMc.startMs,\n end_ms: now,\n duration_sec: durationSec,\n stoppage_duration_seconds: durationSec,\n from_mold_id: closedMc.fromMoldId,\n to_mold_id: closedMc.toMoldId,\n incidentKey: \"mold-change:\" + (closedMc.startMs || now)\n }\n },\n tsMs: now\n };\n const uiClose = { _mode: \"mold-change-status\", payload: { active: false, endMs: now, durationSec } };\n node.send([null, uiClose, null, null, null, closeEvent]);\n}\n\nconst mode = msg._mode || '';\nswitch (msg.action) {\n case \"upload-excel\":\n msg._mode = \"upload\";\n return finalize([msg, null, null, null]);\n case \"refresh-work-orders\": {\n const selectMsg = {\n ...msg,\n _mode: \"select\",\n topic: \"SELECT * FROM work_orders ORDER BY created_at DESC;\"\n };\n const fetchMsg = {\n ...msg,\n topic: \"workOrdersRefresh\",\n forceRefresh: true,\n payload: msg.payload && typeof msg.payload === \"object\" ? msg.payload : {}\n };\n return finalize([null, selectMsg, null, null, null, null, fetchMsg]);\n }\n case \"start-work-order\": {\n msg._mode = \"start-check-progress\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for start\", msg);\n return finalize([null, null, null, null]);\n }\n\n // Store order data temporarily for after DB query\n flow.set(\"pendingWorkOrder\", order);\n\n // Query database to check for existing progress\n msg.topic = \"SELECT cycle_count, good_parts, scrap_parts, progress_percent, target_qty, cavities_total, cavities_active FROM work_orders WHERE work_order_id = ? LIMIT 1\";\n msg.payload = [order.id];\n\n //(`[START-WO] Checking progress for WO ${order.id}`);\n return finalize([null, msg, null, null]);\n }\n case \"resume-work-order\": {\n const now = Date.now();\n state.productionStartTime = now;\n state.actualRunTime = 0;\n state.lastStartChangeTime = now;\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiLastTick = null;\n msg._mode = \"resume\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for resume\", msg);\n return finalize([null, null, null, null]);\n }\n\n //node.warn(`[RESUME-WO] Resuming WO ${order.id} with existing progress`);\n\n // Set status to RUNNING without resetting progress\n msg.topic = \"UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END, cavities_total = COALESCE(NULLIF(?,0), cavities_total), cavities_active = COALESCE(NULLIF(?,0), cavities_active) WHERE status <> 'DONE'\";\n msg.payload = [order.id, order.id, Number(order.cavities_total || order.cavitiesTotal || 0), Number(order.cavities || 0)];\n msg.startOrder = order;\n\n // Load existing values into global state\n // IMPORTANT: Also set scrapParts so goodParts calculation is correct\n order.scrapParts = Number(order.scrapParts) || 0;\n order.goodParts = Number(order.goodParts) || 0;\n\n attachMold(order);\n\n state.activeWorkOrder = order;\n state.cycleCount = Number(order.cycleCount) || 0;\n state.activeOrderHasProgress = true;\n flow.set(\"lastMachineState\", 0);\n state.scrapPromptIssuedFor = null;\n\n //node.warn(`[RESUME-WO] Set cycleCount=${order.cycleCount}, scrapParts=${order.scrapParts}, goodParts=${order.goodParts}`);\n return finalize([null, null, msg, null]);\n }\n case \"restart-work-order\": {\n const now = Date.now();\n state.productionStartTime = now;\n state.actualRunTime = 0;\n state.lastStartChangeTime = now;\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiLastTick = null;\n msg._mode = \"restart\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for restart\", msg);\n return finalize([null, null, null, null]);\n }\n\n //node.warn(`[RESTART-WO] Restarting WO ${order.id} - resetting progress to 0`);\n\n // Reset progress in database AND set status to RUNNING\n msg.topic = \"UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, cycle_count = 0, good_parts = 0, scrap_parts = 0, progress_percent = 0, updated_at = NOW(), cavities_total = COALESCE(NULLIF(?,0), cavities_total), cavities_active = COALESCE(NULLIF(?,0), cavities_active) WHERE work_order_id = ? OR status = 'RUNNING'\";\n msg.payload = [order.id, order.id, Number(order.cavities_total || order.cavitiesTotal || 0), Number(order.cavities || 0), order.id];\n msg.startOrder = order;\n\n // Initialize global state to 0\n order.scrapParts = 0;\n order.goodParts = 0;\n\n attachMold(order);\n\n state.activeWorkOrder = order;\n state.cycleCount = 0;\n state.activeOrderHasProgress = false;\n flow.set(\"lastMachineState\", 0);\n state.scrapPromptIssuedFor = null;\n\n //node.warn(`[RESTART-WO] Reset cycleCount=0, scrap=0, good=0`);\n return finalize([null, null, msg, null]);\n }\n case \"complete-work-order\": {\n state.productionStartTime = null;\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiLastTick = null;\n msg._mode = \"complete\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for complete\", msg);\n return finalize([null, null, null, null]);\n }\n\n // Get final values from global state before clearing\n const activeOrder = state.activeWorkOrder || {};\n const finalCycleCount = Number(state.cycleCount || 0);\n const finalGoodParts = Number(activeOrder.goodParts) || 0;\n const finalScrapParts = Number(activeOrder.scrapParts) || 0;\n\n //node.warn(`[COMPLETE] Persisting final values: cycles=${finalCycleCount}, good=${finalGoodParts}, scrap=${finalScrapParts}`);\n\n msg.completeOrder = order;\n\n // SQL: Persist final counts AND set status to DONE\n msg.topic = \"UPDATE work_orders SET status = 'DONE', cycle_count = ?, good_parts = ?, scrap_parts = ?, progress_percent = 100, updated_at = NOW() WHERE work_order_id = ?\";\n msg.payload = [finalCycleCount, finalGoodParts, finalScrapParts, order.id];\n\n // Clear ALL state on completion\n state.activeWorkOrder = null;\n state.activeOrderHasProgress = false;\n state.trackingEnabled = false;\n state.productionStarted = false;\n state.kpiStartupMode = false;\n state.operatingTime = 0;\n state.lastCycleTime = null;\n state.cycleCount = 0;\n flow.set(\"lastMachineState\", 0);\n state.scrapPromptIssuedFor = null;\n state.actualRunTime = 0;\n state.lastStateChangeTime = null;\n\n if (state.lastState && typeof state.lastState === \"object\") {\n state.lastState = {\n ...state.lastState,\n activeWorkOrder: null,\n cycleCount: 0,\n goodParts: 0,\n scrapParts: 0,\n cycleTime: 0,\n actualCycleTime: 0,\n trackingEnabled: false,\n productionStarted: false,\n tsMs: Date.now()\n };\n }\n\n // ============================================================\n // HIGH SCRAP DETECTION\n // ============================================================\n const targetQty = Number(activeOrder.target) || 0;\n const scrapCount = finalScrapParts;\n const scrapPercent = targetQty > 0 ? (scrapCount / targetQty) * 100 : 0;\n\n // Trigger: Scrap > 10% of target quantity\n let anomalyMsg = null;\n if (scrapPercent > 10 && targetQty > 0) {\n const severity = scrapPercent > 25 ? 'critical' : 'warning';\n\n const highScrapAnomaly = {\n anomaly_type: 'high-scrap',\n severity: severity,\n title: `High Waste Detected`,\n description: `Work order completed with ${scrapCount} scrap parts (${scrapPercent.toFixed(1)}% of target ${targetQty}). Why is there so much waste?`,\n data: {\n scrap_count: scrapCount,\n target_quantity: targetQty,\n scrap_percent: Math.round(scrapPercent * 10) / 10,\n good_parts: finalGoodParts,\n total_cycles: finalCycleCount\n },\n kpi_snapshot: {\n oee: (msg.kpis && msg.kpis.oee) || state.currentKPIs?.oee || 0,\n availability: (msg.kpis && msg.kpis.availability) || state.currentKPIs?.availability || 0,\n performance: (msg.kpis && msg.kpis.performance) || state.currentKPIs?.performance || 0,\n quality: (msg.kpis && msg.kpis.quality) || state.currentKPIs?.quality || 0\n },\n work_order_id: order.id,\n cycle_count: finalCycleCount,\n tsMs: Date.now(),\n status: 'active'\n };\n\n //node.warn(`[HIGH SCRAP] Detected ${scrapPercent.toFixed(1)}% scrap on work order ${order.id}`);\n\n // Send to Event Logger (output 5)\n anomalyMsg = {\n topic: \"anomaly-detected\",\n payload: [highScrapAnomaly]\n };\n }\n\n //node.warn('[COMPLETE] Cleared all state flags');\n return finalize([null, null, null, msg, anomalyMsg]);\n }\n case \"get-current-state\": {\n // Single truth for UI: state.lastState\n const s = state.lastState || {};\n\n const validWorkOrders = state.workOrdersById || null;\n if (state.activeWorkOrder?.id && validWorkOrders && !validWorkOrders[state.activeWorkOrder.id]) {\n node.warn(`[STATE] activeWorkOrder ${state.activeWorkOrder.id} not in work order list; clearing`);\n state.activeWorkOrder = null;\n }\n\n const activeOrder = (state.activeWorkOrder && state.activeWorkOrder.id)\n ? state.activeWorkOrder\n : null;\n\n const trackingEnabled =\n (typeof s.trackingEnabled === \"boolean\" ? s.trackingEnabled : (state.trackingEnabled || false));\n\n const productionStarted =\n (typeof s.productionStarted === \"boolean\" ? s.productionStarted : (state.productionStarted || false));\n\n const kpis =\n (s.kpis && typeof s.kpis === \"object\")\n ? s.kpis\n : (state.currentKPIs || { oee: 0, availability: 0, performance: 0, quality: 0 });\n\n const cavities = Number(activeOrder?.cavities ?? state.lastMoldActive ?? s.cavities ?? 0) || null;\n\n msg._mode = \"current-state\";\n msg.payload = {\n machineId: s.machineId ?? config.machineId ?? undefined,\n\n activeWorkOrder: activeOrder,\n\n // add the fields your UI / home tab might show\n cycleCount: s.cycleCount,\n goodParts: s.goodParts,\n scrapParts: s.scrapParts,\n cavities,\n\n cycleTime: s.cycleTime,\n actualCycleTime: s.actualCycleTime,\n\n trackingEnabled,\n productionStarted,\n kpis,\n reasonCatalog,\n\n tsMs: s.tsMs\n };\n\n return finalize([null, msg, null, null]);\n }\n case \"restore-session\": {\n // Query DB for any RUNNING work order on startup\n msg._mode = \"restore-query\";\n msg.topic = \"SELECT * FROM work_orders WHERE status = 'RUNNING' LIMIT 1\";\n msg.payload = [];\n //node.warn('[RESTORE] Checking for running work order on startup');\n return finalize([null, msg, null, null]);\n }\n case \"scrap-entry\": {\n const { id, scrap } = msg.payload || {};\n const scrapNum = Number(scrap) || 0;\n\n if (!id) {\n node.error(\"No work order id supplied for scrap entry\", msg);\n return finalize([null, null, null, null]);\n }\n\n const activeOrder = state.activeWorkOrder;\n if (activeOrder && activeOrder.id === id) {\n activeOrder.scrapParts = (Number(activeOrder.scrapParts) || 0) + scrapNum;\n state.activeWorkOrder = activeOrder;\n }\n\n state.scrapPromptIssuedFor = null;\n\n msg._mode = \"scrap-update\";\n msg.scrapEntry = { id, scrap: scrapNum, reason: null };\n // SQL with bound parameters for safety\n msg.topic = \"UPDATE work_orders SET scrap_parts = scrap_parts + ?, updated_at = NOW() WHERE work_order_id = ?\";\n msg.payload = [scrapNum, id];\n\n return finalize([null, null, msg, null]);\n }\n case \"scrap-entry-with-reason\": {\n const { id, scrap } = msg.payload || {};\n const scrapNum = Number(scrap) || 0;\n const reason = normalizeReason(msg.payload || {}, \"scrap\");\n\n if (!id) {\n node.error(\"No work order id supplied for scrap entry with reason\", msg);\n return finalize([null, null, null, null, null, null]);\n }\n\n const activeOrder = state.activeWorkOrder;\n if (activeOrder && activeOrder.id === id) {\n activeOrder.scrapParts = (Number(activeOrder.scrapParts) || 0) + scrapNum;\n state.activeWorkOrder = activeOrder;\n }\n\n state.scrapPromptIssuedFor = null;\n state.lastScrapReasonByOrder = state.lastScrapReasonByOrder || {};\n state.lastScrapReasonByOrder[id] = reason;\n\n const tsMs = Date.now();\n const outEvent = {\n tsMs,\n eventType: \"scrap-manual-entry\",\n workOrderId: id,\n scrapDelta: scrapNum,\n source: \"home-ui\",\n // Keep compatibility for current event ingest.\n reason,\n // Explicit split marker for future downtime-specific stream.\n downtime: null\n };\n\n msg._mode = \"scrap-update\";\n msg.scrapEntry = { id, scrap: scrapNum, reason };\n msg.topic = \"UPDATE work_orders SET scrap_parts = scrap_parts + ?, updated_at = NOW() WHERE work_order_id = ?\";\n msg.payload = [scrapNum, id];\n\n // Output 3 -> DB update path, Output 6 -> event outbox builder path.\n return finalize([null, null, msg, null, null, { payload: outEvent, tsMs }]);\n }\n case \"scrap-open\": {\n const active = state.activeWorkOrder || null;\n if (!active?.id) return finalize([null, null, null, null, null]);\n\n const good = Number(active.goodParts) || 0;\n const scrap = Number(active.scrapParts) || 0;\n const produced = good + scrap;\n\n msg._mode = \"scrap-prompt\";\n msg.scrapPrompt = {\n id: active.id,\n sku: active.sku || \"\",\n target: Number(active.target) || 0,\n produced,\n scrapSoFar: scrap,\n\n manual: true,\n title: \"Registrar scrap (parcial)\",\n enterMode: true,\n validateMax: false,\n reasonCatalog: reasonCatalog\n };\n\n // IMPORTANT: don't set msg.topic to a string here\n delete msg.topic;\n delete msg.payload;\n\n // Output 2 feeds Back to UI in your wiring\n return finalize([null, msg, null, null, null]);\n }\n\n\n case \"scrap-skip\": {\n const { id, remindAgain } = msg.payload || {};\n\n if (!id) {\n node.error(\"No work order id supplied for scrap skip\", msg);\n return finalize([null, null, null, null]);\n }\n\n if (remindAgain) {\n state.scrapPromptIssuedFor = null;\n }\n\n msg._mode = \"scrap-skipped\";\n return finalize([null, null, null, null]);\n }\n case \"start\": {\n // START with KPI timestamp init - FIXED\n const now = Date.now();\n const shifts = settings.shifts || [{ start: '06:00', end: '13:00' }];\n const shiftChangeComp = settings.shiftChangeCompensation || 10; // minutes\n const lunchBreak = settings.lunchBreakMinutes || 30; // minutes\n\n\n let totalShiftSeconds = 0;\n shifts.forEach(shift => {\n const [startH, startM] = (shift.start || '06:00').split(':').map(Number);\n const [endH, endM] = (shift.end || '15:00').split(':').map(Number);\n\n let startMinutes = startH * 60 + startM;\n let endMinutes = endH * 60 + endM;\n\n // Handle overnight shifts\n if (endMinutes <= startMinutes) {\n endMinutes += 24 * 60;\n }\n\n totalShiftSeconds += (endMinutes - startMinutes) * 60;\n });\n const compensationSeconds = shifts.length * shiftChangeComp * 60; // shift change per shift\n const lunchSeconds = lunchBreak * 60;\n const plannedProductionTime = Math.max(0, totalShiftSeconds - compensationSeconds - lunchSeconds);\n state.plannedProductionTime = plannedProductionTime;\n\n const existingCycles = Number(state.cycleCount || 0);\n const activeOrderState = state.activeWorkOrder || {};\n const orderGood = Number(activeOrderState.goodParts) || 0;\n const orderScrap = Number(activeOrderState.scrapParts) || 0;\n\n const hasProgressFlag = state.activeOrderHasProgress;\n\n // We consider there is progress only if this order has produced something\n const hasProgress = (typeof hasProgressFlag === 'boolean')\n ? hasProgressFlag\n : (\n existingCycles > 0 ||\n (orderGood + orderScrap) > 0\n );\n\n if (!hasProgress) {\n state.stopTime = 0; // Initialize stop time\n state.trackingEnabled = true;\n state.trackingEnabled = true;\n state.productionStarted = true;\n state.kpiStartupMode = true;\n state.kpiBuffer = [];\n state.lastKPIRecordTime = now - 60000;\n state.productionStartTime = now;\n state.lastMachineCycleTime = now;\n state.lastCycleTime = now;\n state.operatingTime = 0;\n state.actualRunTime = 0;\n state.lastStartChangeTime = now;\n state.lastCycleCompletionTime = null; // Will be set on first cycle\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiLastTick = null;\n } else {\n state.trackingEnabled = true;\n state.productionStarted = true;\n // Do NOT go back into startup mode\n state.kpiStartupMode = false;\n }\n\n //node.warn('[START] Initialized: trackingEnabled=true, productionStarted=true, kpiStartupMode=true, operatingTime=0');\n //node.warn(`[START] Planned production time: ${(plannedProductionTime / 3600).toFixed(2)} hours`);\n\n const activeOrder = state.activeWorkOrder || {};\n msg._mode = \"production-state\";\n\n msg.payload = msg.payload || {};\n\n msg.trackingEnabled = true;\n msg.productionStarted = true;\n msg.machineOnline = true;\n\n msg.payload.trackingEnabled = true;\n msg.payload.productionStarted = true;\n msg.payload.machineOnline = true;\n\n return finalize([null, msg, null, null]);\n }\n case \"stop\": {\n state.trackingEnabled = false;\n state.productionStarted = false;\n //node.warn('[STOP] Set trackingEnabled=false, productionStarted=false');\n\n // Send UI update so button state reflects change\n msg._mode = \"production-state\";\n msg.payload = msg.payload || {};\n msg.trackingEnabled = false;\n msg.productionStarted = false;\n msg.machineOnline = true;\n msg.payload.trackingEnabled = false;\n msg.payload.productionStarted = false;\n msg.payload.machineOnline = true;\n\n const s = state.lastState || {};\n s.trackingEnabled = false;\n s.productionStarted = false;\n s.tsMs = Date.now();\n state.lastState = s;\n\n return finalize([null, msg, null, null]);\n }\n case \"start-tracking\": {\n const activeOrder = state.activeWorkOrder || {};\n\n if (!activeOrder.id) {\n return finalize([null, { topic: \"alert\", payload: \"Error: No active work order loaded.\" }, null, null]);\n }\n\n const now = Date.now();\n state.trackingEnabled = true;\n\n // reset KPI timers/counters (keep what you had)\n state.kpiBuffer = [];\n state.lastKPIRecordTime = now - 60000;\n state.lastMachineCycleTime = now;\n state.lastCycleTime = now;\n state.operatingTime = 0.001;\n\n // IMPORTANT: do NOT set productionStarted here (that’s machine physical state)\n const stateMsg = {\n _mode: \"production-state\",\n payload: {\n trackingEnabled: true,\n machineOnline: true\n }\n };\n\n return finalize([null, stateMsg, null, null]);\n }\ncase \"start-mold-change\": {\n const now = Date.now();\n const p = msg.payload || {};\n const mc = {\n active: true,\n startMs: now,\n endMs: null,\n fromMoldId: p.fromMoldId || state.lastMoldActive || null,\n toMoldId: null,\n operator: p.operator || null\n };\n state.moldChange = mc;\n // Disable tracking during mold change so KPI/anomaly are paused\n state.trackingEnabled = false;\n state.productionStarted = false;\n const event = {\n payload: {\n anomaly_type: \"mold-change\",\n eventType: \"mold-change\",\n severity: \"info\",\n requires_ack: false,\n title: \"Cambio de molde iniciado\",\n description: \"Inicio de cambio de molde\",\n status: \"active\",\n tsMs: now,\n data: {\n status: \"active\",\n start_ms: now,\n from_mold_id: mc.fromMoldId,\n operator: mc.operator,\n incidentKey: \"mold-change:\" + now\n }\n },\n tsMs: now\n };\n const uiMsg = { _mode: \"mold-change-status\", payload: { active: true, startMs: now, fromMoldId: mc.fromMoldId } };\n return finalize([null, uiMsg, null, null, null, event]);\n }\n case \"end-mold-change\": {\n const now = Date.now();\n const p = msg.payload || {};\n const mc = state.moldChange || {};\n if (!mc.active) {\n return finalize([null, null, null, null, null, null]);\n }\n const durationSec = Math.max(0, Math.round((now - (mc.startMs || now)) / 1000));\n const closed = {\n active: false,\n startMs: mc.startMs,\n endMs: now,\n fromMoldId: mc.fromMoldId || null,\n toMoldId: p.toMoldId || state.lastMoldActive || null,\n durationSec\n };\n state.moldChange = closed;\n const event = {\n payload: {\n anomaly_type: \"mold-change\",\n eventType: \"mold-change\",\n severity: \"info\",\n requires_ack: false,\n title: \"Cambio de molde finalizado\",\n description: \"Duracion: \" + Math.round(durationSec/60) + \" min\",\n status: \"resolved\",\n tsMs: now,\n data: {\n status: \"resolved\",\n start_ms: closed.startMs,\n end_ms: now,\n duration_sec: durationSec,\n stoppage_duration_seconds: durationSec,\n from_mold_id: closed.fromMoldId,\n to_mold_id: closed.toMoldId,\n incidentKey: \"mold-change:\" + (closed.startMs || now)\n }\n },\n tsMs: now\n };\n const uiMsg = { _mode: \"mold-change-status\", payload: { active: false, endMs: now, durationSec } };\n return finalize([null, uiMsg, null, null, null, event]);\n }\n\n}\n","outputs":7,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1150,"y":680,"wires":[["07b66a930e3e5e40"],["ddf6d786b5a7e682","2872629594daf0ed","af0fb4f5a75407ac","8c52b16f9c4671d0","5c8843053781fbd9"],["037e4825e2537ed2","ddf6d786b5a7e682","8c52b16f9c4671d0","17b04a44024da6f2"],["037e4825e2537ed2"],["4a382caf6dadab4c"],["a1e59ef54a91ee50"],["8abddc40064f1f64"]]},{"id":"c7a8bb7adab6838e","type":"link out","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"link out 2","mode":"link","links":["fc87eda3d68142da"],"x":585,"y":300,"wires":[]},{"id":"fc87eda3d68142da","type":"link in","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link in 2","links":["c7a8bb7adab6838e"],"x":1305,"y":540,"wires":[["1af33da0fece65a7"]]},{"id":"a0a8bda316e17f8e","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Build Insert SQL","func":"const rows = Array.isArray(msg.payload) ? msg.payload : [];\n\nconst values = rows\n .map((r) => {\n const id = String(r[\"Work Order ID\"] ?? \"\").trim();\n if (!id) return null;\n\n const sku = String(r[\"SKU\"] ?? \"\").trim();\n const targetQty = Number(r[\"Target Quantity\"]) || 0;\n const cycleTime =\n Number(r[\"Theoretical Cycle Time (Seconds)\"]) || 0;\n\n return [id, sku, targetQty, cycleTime, \"PENDING\"];\n })\n .filter(Boolean);\n\nif (!values.length) {\n return null;\n}\n\nmsg.topic = `\n INSERT INTO work_orders\n (work_order_id, sku, target_qty, cycle_time, status)\n VALUES ?\n ON DUPLICATE KEY UPDATE\n sku = VALUES(sku),\n target_qty = VALUES(target_qty),\n cycle_time = VALUES(cycle_time);\n`;\n\nmsg.payload = [values];\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1800,"y":500,"wires":[["037e4825e2537ed2"]]},{"id":"037e4825e2537ed2","type":"mysql","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","mydb":"fc9634aabefee16b","name":"mariaDB","x":1820,"y":920,"wires":[["4bb96d61f9e3bba2"]]},{"id":"2848a91e7a88cd0b","type":"mysql","z":"8ccf34b55a2afcad","g":"8b73333804b30ee0","mydb":"fc9634aabefee16b","name":"mariaDB (Graph Data)","x":1600,"y":80,"wires":[["89d2d406e3cd96ee"]]},{"id":"2872629594daf0ed","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Back to UI","func":"const mode = msg._mode || '';\nconst started = msg.startOrder || null;\nconst completed = msg.completeOrder || null;\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\n\nconst loadMoldCache = () => {\n let cache = null;\n try {\n cache = global.get(\"moldCache\", \"file\");\n } catch (err) {\n cache = null;\n }\n if (!cache || typeof cache !== \"object\") {\n cache = global.get(\"moldCache\") || {};\n }\n return cache;\n};\n\nconst moldCache = loadMoldCache();\nconst cachedByWorkOrder = (moldCache && typeof moldCache.moldByWorkOrder === \"object\")\n ? moldCache.moldByWorkOrder\n : {};\n\nstate.moldByWorkOrder = state.moldByWorkOrder || cachedByWorkOrder || {};\nif (!Number.isFinite(state.lastMoldActive) && Number.isFinite(moldCache.lastMoldActive)) {\n state.lastMoldActive = moldCache.lastMoldActive;\n}\nif (!Number.isFinite(state.lastMoldTotal) && Number.isFinite(moldCache.lastMoldTotal)) {\n state.lastMoldTotal = moldCache.lastMoldTotal;\n}\n\nconst persistMoldCache = () => {\n const cache = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n };\n global.set(\"moldCache\", cache);\n try {\n global.set(\"moldCache\", cache, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst attachMold = (order) => {\n if (!order || !order.id) return;\n const map = state.moldByWorkOrder || {};\n const mapped = map[order.id] || {};\n const resolved = Number(\n order.cavities ?? mapped.active ?? state.lastMoldActive ?? settings.moldActive ?? 0\n );\n if (!Number.isFinite(resolved) || resolved <= 0) return;\n order.cavities = resolved;\n state.lastMoldActive = resolved;\n map[order.id] = {\n total: Number.isFinite(mapped.total) && mapped.total > 0 ? mapped.total : (state.lastMoldTotal ?? null),\n active: resolved\n };\n state.moldByWorkOrder = map;\n};\n\nconst finalize = (ret) => {\n global.set(\"state\", state);\n persistMoldCache();\n return ret;\n};\nconst toIsoString = (v) => {\n if (!v) return null;\n if (typeof v === \"string\") return v;\n if (v instanceof Date) return v.toISOString();\n if (typeof v.toISOString === \"function\") return v.toISOString(); // date-like\n if (typeof v.toISO === \"function\") return v.toISO(); // luxon\n if (typeof v.format === \"function\") return v.format(); // moment\n return String(v);\n};\n\ndelete msg._mode;\ndelete msg.startOrder;\ndelete msg.completeOrder;\ndelete msg.action;\ndelete msg.filename;\n\n//node.warn(`[ROUTER] mode=\"${mode}\" action=\"${msg.action}\"`);\n\n// ========================================================\n// MODE: UPLOAD\n// ========================================================\nif (mode === \"upload\") {\n msg.topic = \"uploadStatus\";\n msg.payload = { message: \"✅ Work orders uploaded successfully.\" };\n return finalize([msg, null, null, null]);\n}\n\n// ========================================================\n// MODE: SELECT (Load Work Orders)\n// ========================================================\nif (mode === \"select\") {\n const rawRows = Array.isArray(msg.payload) ? msg.payload : [];\n msg.topic = \"workOrdersList\";\n msg.payload = rawRows.map(row => ({\n id: row.work_order_id ?? row.id ?? \"\",\n sku: row.sku ?? \"\",\n target: Number(row.target_qty ?? row.target ?? 0),\n goodParts: Number(row.good_parts ?? row.goodParts ?? 0),\n scrapParts: Number(row.scrap_count ?? row.scrap_parts ?? row.scrapParts ?? 0),\n progressPercent: Number(row.progress_percent ?? row.progress ?? 0),\n status: (row.status ?? \"PENDING\").toUpperCase(),\n lastUpdateIso: toIsoString(row.updated_at ?? row.last_update),\n cycleTime: Number(row.cycle_time ?? row.theoretical_cycle_time ?? 0)\n }));\n return finalize([msg, null, null, null]);\n}\n\n// ========================================================\n// MODE: START WORK ORDER\n// ========================================================\nif (mode === \"start\") {\n const order = started || {};\n attachMold(order);\n const kpis = msg.kpis || state.currentKPIs || {\n oee: 0, availability: 0, performance: 0, quality: 0\n };\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: {\n id: order.id || \"\",\n sku: order.sku || \"\",\n target: Number(order.target) || 0,\n goodParts: Number(order.goodParts) || 0,\n scrapParts: Number(order.scrapParts) || 0,\n cycleTime: Number(order.cycleTime || order.theoreticalCycleTime || 0),\n progressPercent: Number(order.progressPercent) || 0,\n lastUpdateIso: toIsoString(order.lastUpdateIso) || null,\n kpis: kpis\n }\n };\n\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: COMPLETE WORK ORDER\n// ========================================================\nif (mode === \"complete\") {\n const homeMsg = { topic: \"activeWorkOrder\", payload: null };\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: CYCLE UPDATE DURING PRODUCTION\n// ========================================================\nif (mode === \"cycle\") {\n const cycle = msg.cycle || {};\n\n const workOrderMsg = {\n topic: \"workOrderCycle\",\n payload: {\n id: cycle.id || \"\",\n sku: cycle.sku || \"\",\n target: Number(cycle.target) || 0,\n goodParts: Number(cycle.goodParts) || 0,\n scrapParts: Number(cycle.scrapParts) || 0,\n progressPercent: Number(cycle.progressPercent) || 0,\n lastUpdateIso: toIsoString(cycle.lastUpdateIso) || null,\n status: cycle.progressPercent >= 100 ? \"DONE\" : \"RUNNING\"\n }\n };\n\n const kpis = msg.kpis || state.currentKPIs || {\n oee: 0, availability: 0, performance: 0, quality: 0\n };\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: {\n id: cycle.id || \"\",\n sku: cycle.sku || \"\",\n target: Number(cycle.target) || 0,\n goodParts: Number(cycle.goodParts) || 0,\n scrapParts: Number(cycle.scrapParts) || 0,\n cycleTime: Number(cycle.cycleTime) || 0,\n progressPercent: Number(cycle.progressPercent) || 0,\n lastUpdateIso: toIsoString(cycle.lastUpdateIso) || new Date().toISOString(),\n kpis: kpis\n }\n };\n\n return finalize([workOrderMsg, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: MACHINE PRODUCTION STATE\n// ========================================================\nif (mode === \"production-state\") {\n const homeMsg = {\n topic: \"machineStatus\",\n payload: {\n machineOnline: msg.machineOnline ?? true,\n productionStarted: !!msg.productionStarted,\n trackingEnabled: msg.payload?.trackingEnabled ?? msg.trackingEnabled ?? false\n }\n };\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: CURRENT STATE (for tab switch sync)\n// ========================================================\nif (mode === \"current-state\") {\n const state = msg.payload || {};\n const homeMsg = {\n topic: \"currentState\",\n payload: {\n activeWorkOrder: state.activeWorkOrder,\n trackingEnabled: state.trackingEnabled,\n productionStarted: state.productionStarted,\n kpis: state.kpis\n }\n };\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: RESTORE QUERY (startup state recovery)\n// ========================================================\nif (mode === \"restore-query\") {\n const rows = Array.isArray(msg.payload) ? msg.payload : [];\n\n if (rows.length > 0) {\n const row = rows[0];\n const restoredOrder = {\n id: row.work_order_id || row.id || \"\",\n sku: row.sku || \"\",\n target: Number(row.target_qty || row.target || 0),\n goodParts: Number(row.good_parts || row.goodParts || 0),\n scrapParts: Number(row.scrap_parts || row.scrapParts || 0),\n progressPercent: Number(row.progress_percent || 0),\n cycleTime: Number(row.cycle_time || 0),\n lastUpdateIso: toIsoString(row.updated_at ?? row.last_update),\n cavities: Number(row.cavities_active || 0),\n cavities_total: Number(row.cavities_total || 0)\n };\n\n attachMold(restoredOrder);\n\n if (Number(restoredOrder.cavities) > 0) {\n state.lastMoldActive = Number(restoredOrder.cavities);\n }\n if (Number(restoredOrder.cavities_total) > 0) {\n state.lastMoldTotal = Number(restoredOrder.cavities_total);\n }\n if (restoredOrder.id && Number(restoredOrder.cavities) > 0) {\n const map = state.moldByWorkOrder || {};\n map[restoredOrder.id] = {\n total: Number(restoredOrder.cavities_total) > 0 ? Number(restoredOrder.cavities_total) : (state.lastMoldTotal ?? null),\n active: Number(restoredOrder.cavities)\n };\n state.moldByWorkOrder = map;\n }\n\n const restoredCycleCount = Number(row.cycle_count) || 0;\n const restoredGood = Number(row.good_parts || 0);\n const restoredScrap = Number(row.scrap_parts || 0);\n const hasProgress = restoredCycleCount > 0 || (restoredGood + restoredScrap) > 0;\n\n // Restore global state\n state.activeWorkOrder = restoredOrder;\n state.cycleCount = restoredCycleCount;\n // Auto-resume tracking for RUNNING work order\n state.trackingEnabled = true;\n state.productionStarted = true;\n state.kpiStartupMode = !hasProgress;\n\n // Reset tick so KPI integration doesn't jump\n state.kpiLastTick = null;\n\n let restoreTs = Date.parse(row.updated_at || \"\");\n if (!isFinite(restoreTs)) {\n restoreTs = Date.now();\n }\n state.lastMachineCycleTime = restoreTs;\n state.lastCycleCompletionTime = restoreTs;\n\n node.warn('[RESTORE] Restored work order: ' + restoredOrder.id + ' with ' + state.cycleCount + ' cycles');\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: restoredOrder\n };\n\n const stateMsg = {\n topic: \"machineStatus\",\n payload: {\n machineOnline: true,\n productionStarted: true,\n trackingEnabled: true\n }\n };\n\n // Set status back to RUNNING in database (if not already DONE)\n // This prevents user from having to \"Load\" the work order again\n const dbMsg = {\n topic: \"UPDATE work_orders SET status = 'RUNNING', updated_at = NOW() WHERE work_order_id = ? AND status != 'DONE'\",\n payload: [restoredOrder.id]\n };\n\n\n return finalize([dbMsg, [homeMsg, stateMsg], null, null]);\n } else {\n node.warn('[RESTORE] No running work order found');\n }\n return finalize([null, null, null, null]);\n}\n\n// ========================================================\n// MODE: SCRAP PROMPT\n// ========================================================\nif (mode === \"scrap-prompt\") {\n const prompt = msg.scrapPrompt || {};\n\n const homeMsg = { topic: \"scrapPrompt\", payload: prompt };\n const tabMsg = { ui_control: { tab: \"Home\" } };\n\n // output1: nothing\n // output2: home template\n // output3: tab navigation\n // output4: graphs template (unused here)\n return finalize([null, homeMsg, tabMsg, null]);\n}\n\n// ========================================================\n// MODE: SCRAP UPDATE\n// ========================================================\nif (mode === \"scrap-update\") {\n const activeOrder = state.activeWorkOrder || {};\n const kpis = msg.kpis || state.currentKPIs || {\n oee: 0, availability: 0, performance: 0, quality: 0\n };\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: {\n id: activeOrder.id || \"\",\n sku: activeOrder.sku || \"\",\n target: Number(activeOrder.target) || 0,\n goodParts: Number(activeOrder.goodParts) || 0,\n scrapParts: Number(activeOrder.scrapParts) || 0,\n cycleTime: Number(activeOrder.cycleTime) || 0,\n progressPercent: Number(activeOrder.progressPercent) || 0,\n lastUpdateIso: toIsoString(activeOrder.lastUpdateIso) || new Date().toISOString(),\n kpis: kpis\n }\n };\n\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: SCRAP COMPLETE\n// ========================================================\nif (mode === \"scrap-complete\") {\n const homeMsg = { topic: \"activeWorkOrder\", payload: null };\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: CHARTS → SEND REAL DATA TO GRAPH TEMPLATE\n// ========================================================\n//if (mode === \"charts\") {\n\n// const realOEE = msg.realOEE || state.realOEE || [];\n// const realAvailability = msg.realAvailability || state.realAvailability || [];\n// const realPerformance = msg.realPerformance || state.realPerformance || [];\n// const realQuality = msg.realQuality || state.realQuality || [];\n\n// const chartsMsg = {\n// topic: \"chartsData\",\n// payload: {\n// oee: realOEE,\n// availability: realAvailability,\n// performance: realPerformance,\n// quality: realQuality\n// }\n// };\n\n// Send ONLY to output #4\n// return [null, null, null, chartsMsg];\n//}\n\n\n// ========================================================\n// MODE: RESUME-PROMPT\n// ========================================================\nif (mode === \"resume-prompt\") {\n const order = msg.payload.order || null;\n\n if (order) {\n attachMold(order);\n state.activeWorkOrder = order;\n node.warn(\n `[RESUME-PROMPT] activeWorkOrder set to ${order.id}`\n );\n }\n\n const homeMsg = {\n topic: msg.topic || \"resumePrompt\",\n payload: msg.payload,\n };\n\n const kpis =\n msg.kpis ||\n state.currentKPIs || {\n oee: 0,\n availability: 0,\n performance: 0,\n quality: 0,\n };\n\n const activeMsg = order\n ? {\n topic: \"activeWorkOrder\",\n payload: {\n id: order.id || \"\",\n sku: order.sku || \"\",\n target: Number(order.target) || 0,\n goodParts: Number(order.goodParts) || 0,\n scrapParts: Number(order.scrapParts) || 0,\n cycleTime: Number(\n order.cycleTime ||\n order.theoreticalCycleTime ||\n 0\n ),\n progressPercent: Number(\n msg.payload?.progressPercent ??\n order.progressPercent ??\n 0\n ),\n lastUpdateIso: order.lastUpdateIso || null,\n kpis,\n },\n }\n : null;\n\n return finalize([\n null,\n activeMsg ? [activeMsg, homeMsg] : homeMsg,\n null,\n null,\n ]);\n}\n\n\n// ========================================================\n\n// ========================================================\n// MODE: MOLD CHANGE STATUS\n// ========================================================\nif (mode === \"mold-change-status\") {\n const homeMsg = {\n topic: \"moldChangeStatus\",\n payload: msg.payload || {}\n };\n return finalize([null, homeMsg, null, null]);\n}\n\n// DEFAULT\n// ========================================================\nreturn finalize([null, null, null, null]);","outputs":4,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2190,"y":560,"wires":[["0fe0cdc07941b31e"],["21e9ba4f889da77f"],["cb972412abfe909f"],[]]},{"id":"0fe0cdc07941b31e","type":"link out","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link out 3","mode":"link","links":["8f16fdca6d059017"],"x":2305,"y":520,"wires":[]},{"id":"8f16fdca6d059017","type":"link in","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"link in 3","links":["0fe0cdc07941b31e"],"x":275,"y":320,"wires":[["405dccd3e09a99d4"]]},{"id":"5df1ee6ee335a44a","type":"book","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"","raw":false,"x":1810,"y":460,"wires":[["a75b33fb4e9ebdf8"]]},{"id":"a75b33fb4e9ebdf8","type":"sheet","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"","sheetName":"Sheet1","x":1930,"y":460,"wires":[["7cb1c8d40f510e0b"]]},{"id":"7cb1c8d40f510e0b","type":"sheet-to-json","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"","raw":"false","range":"","header":"default","blankrows":false,"x":2070,"y":460,"wires":[["a0a8bda316e17f8e"]]},{"id":"07b66a930e3e5e40","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Base64","func":"const filename =\n msg.filename ||\n (msg.meta && msg.meta.filename) ||\n (msg.payload && msg.payload.filename) ||\n msg.name ||\n 'upload.xlsx';\n\nconst candidates = [];\nif (typeof msg.payload === 'string') candidates.push(msg.payload);\nif (msg.payload && typeof msg.payload.payload === 'string') candidates.push(msg.payload.payload);\nif (msg.payload && typeof msg.payload.file === 'string') candidates.push(msg.payload.file);\nif (msg.payload && typeof msg.payload.base64 === 'string') candidates.push(msg.payload.base64);\nif (typeof msg.file === 'string') candidates.push(msg.file);\nif (typeof msg.data === 'string') candidates.push(msg.data);\n\nfunction stripDataUrl(s) {\n return (s && s.startsWith('data:')) ? s.split(',')[1] : s;\n}\n\nlet b64 = candidates.map(stripDataUrl).find(s => typeof s === 'string' && s.length > 0);\nif (!b64 && Buffer.isBuffer(msg.payload)) { msg.filename = filename; return msg; }\nif (!b64) { node.error('No base64 data found on msg', msg); return null; }\n\nmsg.payload = Buffer.from(b64, 'base64');\nmsg.filename = filename;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1680,"y":460,"wires":[["5df1ee6ee335a44a"]]},{"id":"21e9ba4f889da77f","type":"link out","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link out 4","mode":"link","links":["47a6ea4de26fadce"],"x":2305,"y":560,"wires":[]},{"id":"47a6ea4de26fadce","type":"link in","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"link in 4","links":["21e9ba4f889da77f"],"x":275,"y":280,"wires":[["87dc48bbe753ab8d"]]},{"id":"97399876ca829d1a","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Refresh Trigger","func":"if (msg._mode === \"sync-work-orders\") {\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, null];\n}\nif (msg._mode === \"start\" || msg._mode === \"complete\" || msg._mode === \"resume\" || msg._mode === \"restart\") {\n // Preserve original message for Back to UI (output 2)\n const originalMsg = {...msg};\n // Create select message for refreshing WO table (output 1)\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, originalMsg];\n}\nif (msg._mode === \"cycle\" || msg._mode === \"production-state\") {\n return [null, msg];\n}\nif (msg._mode === \"scrap-prompt\") {\n return [null, msg];\n}\nif (msg._mode === \"restore-query\") {\n // Pass restore query results to Back to UI\n return [null, msg];\n}\nif (msg._mode === \"current-state\") {\n // Pass current state to Back to UI\n return [null, msg];\n}\nif (msg._mode === \"scrap-complete\") {\n // Preserve original message for Back to UI (output 2)\n const originalMsg = {...msg};\n // Create select message for refreshing WO table (output 1)\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, originalMsg];\n}\nreturn [null, msg];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1960,"y":600,"wires":[["037e4825e2537ed2"],["2872629594daf0ed"]]},{"id":"ac25819b6b50ab90","type":"function","z":"8ccf34b55a2afcad","g":"c95c05b78d8464c5","name":"Machine cycles","func":"const current = Number(msg.payload) || 0;\nconst now = Date.now();\n\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\nconst saveState = () => global.set(\"state\", state);\nconst finalize = (ret) => { saveState(); return ret; };\n\nlet zeroStreak = flow.get(\"zeroStreak\") || 0;\nzeroStreak = current === 0 ? zeroStreak + 1 : 0;\nflow.set(\"zeroStreak\", zeroStreak);\n\nconst prev = flow.get(\"lastMachineState\") ?? 0;\n\n// =============================================\n// TRACK ACTUAL RUN TIME (must happen BEFORE any early returns)\n// Only track if we have an active work order and tracking is enabled\n// =============================================\nconst activeOrder = state.activeWorkOrder;\nconst trackingEnabled = !!state.trackingEnabled;\n\n//if (trackingEnabled && activeOrder && activeOrder.id) {\n // =============================================\n // THRESHOLD-BASED STOPPAGE DETECTION\n // Runs on EVERY state change to detect gaps\n // =============================================\n /*\n const lastCycleTime = state.lastCycleCompletionTime || now;\n const timeSinceLastCycle = now - lastCycleTime;\n const deltaSeconds = timeSinceLastCycle / 1000;\n \n const thresholdMultiplier = settings.thresholdMultiplier || 1.5;\n const Tideal = Number(activeOrder.cycleTime) || 5; // seconds\n const ThresholdParo = Tideal * thresholdMultiplier;\n \n // Only analyze gaps when we have a previous cycle to compare against\n // and when transitioning TO state=1 (cycle completion)\n if (current === 1 && prev === 0 && state.lastCycleCompletionTime) {\n let operatingTime = state.operatingTime || 0;\n let stopTime = state.stopTime || 0;\n \n if (deltaSeconds <= ThresholdParo) {\n // Normal operation - all time counts as running\n operatingTime += deltaSeconds;\n } else {\n // Gap detected - split into running + stopped\n operatingTime += Tideal; // Credit one ideal cycle worth (machine was running for that)\n stopTime += (deltaSeconds - Tideal); // Rest is unplanned downtime\n node.warn(`[STOPPAGE] Detected ${(deltaSeconds - Tideal).toFixed(1)}s downtime (gap: ${deltaSeconds.toFixed(1)}s, threshold: ${ThresholdParo.toFixed(1)}s)`);\n }\n \n state.operatingTime = operatingTime;\n state.stopTime = stopTime;\n }\n \n // =============================================\n // LEGACY: State-based run time tracking (keep as backup/comparison)\n // =============================================\n if (prev === 1) {\n const lastStateChange = state.lastStateChangeTime || now;\n const runDuration = (now - lastStateChange) / 1000;\n\n if (runDuration > 0 && runDuration < 3600) {\n let actualRunTime = state.actualRunTime || 0;\n actualRunTime += runDuration;\n state.actualRunTime = actualRunTime;\n }\n }\n */\n//}\n\n// ALWAYS update state tracking (before any early returns)\nstate.lastStateChangeTime = now;\nflow.set(\"lastMachineState\", current);\n\n// =============================================\n// MACHINE ONLINE STATUS\n// =============================================\nstate.machineOnline = true;\n\nlet productionRunning = !!state.productionStarted;\nlet stateChanged = false;\n\nif (current === 1 && !productionRunning) {\n productionRunning = true;\n stateChanged = true;\n} else if (current === 0 && zeroStreak >= 2 && productionRunning) {\n productionRunning = false;\n stateChanged = true;\n}\n\nstate.productionStarted = productionRunning;\n\nconst stateMsg = stateChanged\n ? {\n _mode: \"production-state\",\n machineOnline: true,\n productionStarted: productionRunning\n }\n : null;\n\n// =============================================\n// EARLY EXIT CONDITIONS\n// =============================================\nconst cavities = Number(\n activeOrder?.cavities ??\n state.moldByWorkOrder?.[activeOrder?.id]?.active ??\n state.lastMoldActive ??\n settings.moldActive ??\n global.get(\"moldActive\") ??\n 0\n);\n\nnode.warn(`[CYCLE-DBG] cur=${current} prev=${prev} wo=${activeOrder?.id} cav=${cavities} | aWO.cav=${activeOrder?.cavities} mBWO=${state.moldByWorkOrder?.[activeOrder?.id]?.active} lMA=${state.lastMoldActive} sMA=${settings.moldActive} gMA=${global.get(\"moldActive\")}`);\n\nif (!activeOrder || !activeOrder.id || !Number.isFinite(cavities) || cavities <= 0) {\n return finalize([null, stateMsg, { _triggerKPI: true }, null]);\n}\n\nactiveOrder.cavities = cavities;\n\nif (!trackingEnabled) {\n return finalize([null, stateMsg, { _triggerKPI: true }, null]);\n}\n\n// Only count cycles on rising edge (0→1)\nif (prev === 1 || current !== 1) {\n return finalize([null, stateMsg, { _triggerKPI: true }, null]);\n}\n\n// =============================================\n// CYCLE COUNTING (only reached on 0→1 transition)\n// =============================================\n\nconst lastCompletion = state.lastCycleCompletionTime;\nif (lastCompletion) {\n const actualCycleTime = (now - lastCompletion) / 1000; // seconds\n state.lastActualCycleTime = actualCycleTime;\n}\n\nlet cycles = Number(state.cycleCount || 0) + 1;\nstate.cycleCount = cycles;\n// Track when this cycle completed (for gap analysis)\nstate.lastCycleCompletionTime = now;\n\n// Clear startup mode after first real cycle\nif (state.kpiStartupMode) {\n state.kpiStartupMode = false;\n node.warn('[MACHINE CYCLE] First cycle - cleared kpiStartupMode');\n}\n\nstate.lastMachineCycleTime = now;\nstate.saveKpis = 1\n\n// =============================================\n// PRODUCTION METRICS\n// =============================================\nconst scrapTotal = Number(activeOrder.scrapParts) || 0;\nconst totalProduced = cycles * cavities;\nconst produced = Math.max(0, totalProduced - scrapTotal);\nconst target = Number(activeOrder.target) || 0;\nconst progress = target > 0 ? Math.min(100, Math.round((produced / target) * 100)) : 0;\n\nactiveOrder.goodParts = produced;\nactiveOrder.progressPercent = progress;\nactiveOrder.lastUpdateIso = new Date().toISOString();\nstate.activeWorkOrder = activeOrder;\n\n// =============================================\n// SCRAP PROMPT CHECK\n// =============================================\nconst promptIssued = state.scrapPromptIssuedFor || null;\nif (!promptIssued && target > 0 && produced >= target) {\n state.scrapPromptIssuedFor = activeOrder.id;\n msg._mode = \"scrap-prompt\";\n msg.scrapPrompt = {\n id: activeOrder.id,\n sku: activeOrder.sku || \"\",\n target,\n produced\n };\n return finalize([null, msg, null, null]);\n}\n\n// =============================================\n// DATABASE UPDATE\n// =============================================\nconst dbMsg = {\n _mode: \"cycle\",\n cycle: {\n id: activeOrder.id,\n sku: activeOrder.sku || \"\",\n target,\n goodParts: produced,\n scrapParts: scrapTotal,\n cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0),\n progressPercent: progress,\n lastUpdateIso: activeOrder.lastUpdateIso,\n machineOnline: true,\n productionStarted: productionRunning\n },\n topic: \"UPDATE work_orders SET good_parts = ?, progress_percent = ?, updated_at = NOW() WHERE work_order_id = ?\",\n payload: [produced, progress, activeOrder.id]\n};\n\nconst kpiTrigger = { _triggerKPI: true };\n\nconst persistWorkOrder = {\n topic: \"UPDATE work_orders SET cycle_count = ?, good_parts = ?, scrap_parts = ?, progress_percent = ?, updated_at = NOW() WHERE work_order_id = ?\",\n payload: [cycles, produced, scrapTotal, progress, activeOrder.id]\n};\n\ndbMsg.cycleRow = {\n tsMs: now, // ms epoch\n cycle_count: cycles, // cycle counter\n actual_cycle_time: Number(state.lastActualCycleTime || 0),\n theoretical_cycle_time: Number(activeOrder.cycleTime || 0),\n work_order_id: String(activeOrder.id),\n sku: String(activeOrder.sku || \"\"),\n cavities: cavities,\n good_delta: cavities, // optional (1 cycle worth)\n scrap_total: scrapTotal, // optional\n};\n\n\nreturn finalize([dbMsg, stateMsg, kpiTrigger, persistWorkOrder]);","outputs":4,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1860,"y":220,"wires":[["0319de4ecd45bdbf","d685745ab029bddb","917065027876ecd6"],["cf7c59785d89fa2a","5c86dbb1f8044c9b"],[],["30be157b60fc55fd"]]},{"id":"0319de4ecd45bdbf","type":"link out","z":"8ccf34b55a2afcad","g":"c95c05b78d8464c5","name":"link out 5","mode":"link","links":["6f993b8cb99de446"],"x":2015,"y":200,"wires":[]},{"id":"6f993b8cb99de446","type":"link in","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link in 5","links":["0319de4ecd45bdbf"],"x":1525,"y":480,"wires":[["037e4825e2537ed2"]]},{"id":"cf7c59785d89fa2a","type":"link out","z":"8ccf34b55a2afcad","g":"c95c05b78d8464c5","name":"link out 6","mode":"link","links":["86abdbe356524296"],"x":2035,"y":240,"wires":[]},{"id":"86abdbe356524296","type":"link in","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link in 6","links":["cf7c59785d89fa2a"],"x":1755,"y":580,"wires":[["97399876ca829d1a"]]},{"id":"cb972412abfe909f","type":"link out","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link out 7","mode":"link","links":["de82c1e664a73bfb"],"x":2305,"y":600,"wires":[]},{"id":"de82c1e664a73bfb","type":"link in","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"link in 7","links":["cb972412abfe909f"],"x":535,"y":420,"wires":[["6adb9fd7e98f4274"]]},{"id":"6b1a37faf14b1a55","type":"link out","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","name":"link out 8","mode":"link","links":["a51caffa7492c805"],"x":955,"y":660,"wires":[]},{"id":"a51caffa7492c805","type":"link in","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"link in 8","links":["6b1a37faf14b1a55","b45e6bfabde1f79c"],"x":275,"y":480,"wires":[["372edd599e1b2fdd"]]},{"id":"ddf6d786b5a7e682","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Calculate KPIs","func":"// ========================================\n// KPI HEARTBEAT - Continuous OEE calculation\n// Runs every second from a dedicated inject node\n// ========================================\n\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\n\n// 1) Basic guards: only run when tracking an active work order\nconst trackingEnabled = state.trackingEnabled || false;\nconst activeOrder = state.activeWorkOrder || {};\nif (!trackingEnabled || !activeOrder.id) {\n return null;\n}\n\n// Detect work-order change and restore/reset KPI counters\nconst currentId = activeOrder.id;\nif (state.kpiOrderId !== currentId) {\n const byOrder = state.kpiByWorkOrder || {};\n const saved = byOrder[currentId];\n\n if (saved && state.activeOrderHasProgress) {\n state.productionStartTime = saved.productionStartTime;\n state.runSeconds = saved.runSeconds;\n state.stopSeconds = saved.stopSeconds;\n state.kpiStartupMode = !!saved.kpiStartupMode;\n state.kpiLastTick = null; // avoid huge dt jump\n } else {\n state.productionStartTime = Date.now();\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiStartupMode = true;\n state.kpiLastTick = null;\n }\n\n state.kpiOrderId = currentId;\n state.perf = { lastCycleCount: 0, runSeconds: 0 };\n\n}\n\n\n// Production must have a start time\n//const productionStartTime = state.productionStartTime;\n//if (!productionStartTime) {\n// return null;\n//}\n\n// Optional startup mode: keep 100/100/100/100 until first real cycle\nlet kpiStartupMode = !!state.kpiStartupMode;\n\nif (kpiStartupMode && Number(state.cycleCount || 0) > 0) {\n state.kpiStartupMode = false;\n kpiStartupMode = false;\n}\n\n// 2) Time step (dt) since last KPI tick\nconst now = Date.now();\nlet lastTick = state.kpiLastTick;\nlet dt;\nif (!lastTick) {\n dt = 0; // first run: don't integrate any time yet\n} else {\n dt = (now - lastTick) / 1000;\n}\nstate.kpiLastTick = now;\n\n// Clamp weird dt values (e.g. after long pause)\nif (dt < 0 || dt > 5) {\n dt = 1;\n}\nlet productionStartTime = state.productionStartTime;\nif (!productionStartTime) {\n // if we’re tracking a valid order, start the clock now\n productionStartTime = now;\n state.productionStartTime = productionStartTime;\n}\n\n// 3) Scheduled production time so far (denominator for Availability)\nlet elapsedSeconds = (now - productionStartTime) / 1000;\nif (elapsedSeconds < 0) elapsedSeconds = 0;\n\nconst plannedProductionTime = Number(state.plannedProductionTime || 0);\n\n// scheduledSeconds = time in the shift that *should* be production\nlet scheduledSeconds = elapsedSeconds;\nif (plannedProductionTime > 0) {\n scheduledSeconds = Math.min(elapsedSeconds, plannedProductionTime);\n}\n\n// 4) Determine machine state from last cycle timestamp\nconst lastCycleTime = state.lastMachineCycleTime || null;\nlet machineState = state.machineState || \"IDLE\";\n\nif (lastCycleTime) {\n const sinceLastCycle = (now - lastCycleTime) / 1000;\n const idealCycleTime = Number(activeOrder.cycleTime) || 1;\n const thresholdMultiplier = Number(settings.thresholdMultiplier || 1.5);\n const stopThreshold = idealCycleTime * thresholdMultiplier;\n\n machineState = (sinceLastCycle <= stopThreshold) ? \"RUNNING\" : \"STOPPED\";\n state.machineState = machineState;\n\n if (kpiStartupMode && sinceLastCycle > stopThreshold) {\n state.kpiStartupMode = false;\n kpiStartupMode = false;\n }\n}\n// ---- Shift-based availability (daily) ----\nconst shifts = settings.shifts || [{ start: '06:00', end: '15:00' }];\nconst shiftChangeComp = Number(settings.shiftChangeCompensation ?? 10);\nconst lunchBreak = Number(settings.lunchBreakMinutes ?? 30);\n\nfunction hmToMinutes(hm) {\n const parts = String(hm || '00:00').split(':').map(Number);\n return (parts[0] || 0) * 60 + (parts[1] || 0);\n}\nfunction isInShift(nowMs) {\n const d = new Date(nowMs);\n const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0).getTime();\n return shifts.some(s => {\n let startM = hmToMinutes(s.start);\n let endM = hmToMinutes(s.end);\n let start = dayStart + startM * 60000;\n let end = dayStart + endM * 60000;\n if (end <= start) end += 24 * 60 * 60000; // overnight\n return nowMs >= start && nowMs <= end;\n });\n}\nfunction plannedDaySeconds() {\n let total = 0;\n shifts.forEach(s => {\n let startM = hmToMinutes(s.start);\n let endM = hmToMinutes(s.end);\n if (endM <= startM) endM += 24 * 60;\n total += (endM - startM) * 60;\n });\n const compensation = shifts.length * shiftChangeComp * 60;\n const lunch = lunchBreak * 60;\n return Math.max(0, total - compensation - lunch);\n}\n\nconst d = new Date(now);\nconst dayKey = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;\nif (state.availDayKey !== dayKey) {\n state.availDayKey = dayKey;\n state.availDowntimeSeconds = 0;\n}\n\nconst inShift = isInShift(now);\nconst plannedSeconds = plannedDaySeconds();\nif (inShift && dt > 0 && machineState !== \"RUNNING\") {\n state.availDowntimeSeconds = Number(state.availDowntimeSeconds || 0) + dt;\n}\nconst availabilityPct = plannedSeconds > 0\n ? ((plannedSeconds - (state.availDowntimeSeconds || 0)) / plannedSeconds) * 100\n : 0;\n\n\n// 5) Integrate run / stop time (numerators)\n// We keep these as smooth, per-second counters\nlet runSeconds = Number(state.runSeconds || 0);\nlet stopSeconds = Number(state.stopSeconds || 0);\n\n// During startup mode (before first cycle), don't integrate anything\nif (!kpiStartupMode && dt > 0 && scheduledSeconds > 0) {\n if (machineState === \"RUNNING\") {\n runSeconds += dt;\n } else if (machineState === \"STOPPED\") {\n stopSeconds += dt;\n }\n}\n\nstate.runSeconds = runSeconds;\nstate.stopSeconds = stopSeconds;\n\n// 6) Pull counters for Performance & Quality\nconst cycleCount = Number(state.cycleCount || 0);\nconst cavities = Number(\n activeOrder.cavities ??\n (state.moldByWorkOrder?.[activeOrder.id]?.active) ??\n state.lastMoldActive ??\n settings.moldActive ??\n 0\n);\nconst totalParts = cycleCount * cavities;\nconst totalCycles = cycleCount;\n\n\nconst scrapTotal = Number(activeOrder.scrapParts) || 0;\nconst goodParts = Math.max(0, totalParts - scrapTotal);\n\nconst idealCycleTime = Number(activeOrder.cycleTime) || 1;\n\n// 7) Compute KPIs\n\n// Helper to keep values sane\nfunction clampPercent(v) {\n if (!isFinite(v) || isNaN(v)) return 0;\n return Math.max(0, Math.min(100, v));\n}\n\nlet availability = 0;\nlet performance = 0;\nlet quality = 100;\nlet oee = 0;\n\n// Startup mode: skip KPI emit until first real cycle/state transition\nif (kpiStartupMode) {\n global.set(\"state\", state);\n return null;\n} else {\n // Availability = Run Time / Scheduled Production Time\n availability = availabilityPct;\n\n\n // Performance = (Ideal Cycle Time × Cycles) / Sum of actual cycle times\n const actualCycleTime = Number(state.lastActualCycleTime || 0);\n state.perf = state.perf || { lastCycleCount: 0, runSeconds: 0 };\n\n // Use same threshold you use for slow-cycle\n const perfThreshold = idealCycleTime * Number(settings.thresholdMultiplier || 1.5);\n\n if (cycleCount > state.perf.lastCycleCount && actualCycleTime > 0) {\n // Cap at threshold so time beyond becomes downtime, not performance loss\n const perfCycleTime = Math.min(actualCycleTime, perfThreshold);\n state.perf.runSeconds += perfCycleTime;\n state.perf.lastCycleCount = cycleCount;\n }\n\n if (state.perf.runSeconds > 0 && cycleCount > 0) {\n const idealTime = idealCycleTime * cycleCount;\n performance = (idealTime / state.perf.runSeconds) * 100;\n }\n\n // Quality = Good Parts / Total Parts\n if (totalParts > 0) {\n quality = (goodParts / totalParts) * 100;\n }\n\n availability = clampPercent(availability);\n performance = clampPercent(performance);\n quality = clampPercent(quality);\n\n // OEE = A × P × Q\n oee = (availability * performance * quality) / 10000;\n}\n\n// Clamp & round to 1 decimal\noee = clampPercent(oee);\n\nmsg.kpis = {\n availability: Math.round(availability * 10) / 10,\n performance: Math.round(performance * 10) / 10,\n quality: Math.round(quality * 10) / 10,\n oee: Math.round(oee * 10) / 10\n};\n\nstate.currentKPIs = msg.kpis;\nstate.kpiByWorkOrder = state.kpiByWorkOrder || {};\nstate.kpiByWorkOrder[activeOrder.id] = {\n productionStartTime: state.productionStartTime,\n runSeconds: state.runSeconds,\n stopSeconds: state.stopSeconds,\n kpiStartupMode: state.kpiStartupMode,\n kpiLastTick: state.kpiLastTick\n};\nglobal.set(\"state\", state);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1380,"y":680,"wires":[["97399876ca829d1a","35542f4dc31c3a96","ea175b8208d85ed5","c323fd9c710bcded","c48444f30fc71bae"]]},{"id":"851a0e837a94ed78","type":"function","z":"8ccf34b55a2afcad","g":"1f2e45c551f40615","name":"Process Alert for DB","func":"// Process incoming alert\nif (msg.payload && msg.payload.action === 'alert') {\n const alert = msg.payload;\n\n const tsMs = typeof alert.tsMs === \"number\" ? alert.tsMs : Date.now();\n const timestamp = new Date(tsMs).toISOString().slice(0, 19).replace('T', ' ');\n\n // Prepare INSERT query\n const alertType = (alert.type || 'Unknown').replace(/'/g, \"''\"); // Escape quotes\n const description = (alert.description || '').replace(/'/g, \"''\"); // Escape quotes\n\n msg.topic = `\n INSERT INTO alerts_log (timestamp, alert_type, description)\n VALUES ('${timestamp}', '${alertType}', '${description}')\n `;\n\n node.status({\n fill: 'green',\n shape: 'dot',\n text: `Logging: ${alertType}`\n });\n\n // Store original message for passthrough\n msg._originalAlert = alert;\n\n return msg;\n}\n\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":860,"wires":[["6f2897368cb48370"]]},{"id":"6f2897368cb48370","type":"mysql","z":"8ccf34b55a2afcad","g":"1f2e45c551f40615","mydb":"fc9634aabefee16b","name":"Log Alert to DB","x":640,"y":860,"wires":[[]]},{"id":"76598272c6e67323","type":"link in","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"link in 9","links":["ca6e4da8d0c5f853","52501193c1677944"],"x":275,"y":400,"wires":[["6feab0f06c780f4c"]]},{"id":"ca6e4da8d0c5f853","type":"link out","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link out 9","mode":"link","links":["76598272c6e67323"],"x":1935,"y":660,"wires":[]},{"id":"35542f4dc31c3a96","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Record KPI History","func":"// Complete Record KPI History function with robust initialization and averaging\n\nconst state = global.get(\"state\") || {};\nconst saveState = () => global.set(\"state\", state);\n\n// ========== INITIALIZATION ==========\n// Initialize buffer\nlet buffer = state.kpiBuffer;\nif (!buffer || !Array.isArray(buffer)) {\n buffer = [];\n state.kpiBuffer = buffer;\n node.warn('[KPI History] Initialized kpiBuffer');\n}\n\n// Initialize last record time\nlet lastRecordTime = state.lastKPIRecordTime;\nif (!lastRecordTime || typeof lastRecordTime !== 'number') {\n // Set to 1 minute ago to ensure immediate recording on startup\n lastRecordTime = Date.now() - 60000;\n state.lastKPIRecordTime = lastRecordTime;\n node.warn('[KPI History] Initialized lastKPIRecordTime');\n}\n\n// ========== ACCUMULATE ==========\nconst kpis = msg.payload?.kpis || msg.kpis;\nif (!kpis) {\n node.warn('[KPI History] No KPIs in message, skipping');\n saveState();\n return null;\n}\n\nbuffer.push({\n tsMs: Date.now(),\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n});\n\n// Prevent buffer from growing too large (safety limit)\nif (buffer.length > 100) {\n buffer = buffer.slice(-60); // Keep last 60 entries\n node.warn('[KPI History] Buffer exceeded 100 entries, trimmed to 60');\n}\n\nstate.kpiBuffer = buffer;\nsaveState();\n\n// ========== CHECK IF TIME TO RECORD ==========\nconst now = Date.now();\nconst timeSinceLastRecord = now - lastRecordTime;\nconst ONE_MINUTE = 60 * 1000;\n\nif (timeSinceLastRecord < ONE_MINUTE) {\n // Not time to record yet\n return null; // Don't send to charts yet\n}\n\n// ========== CALCULATE AVERAGES ==========\nif (buffer.length === 0) {\n node.warn('[KPI History] Buffer empty at recording time, skipping');\n return null;\n}\n\nconst avg = {\n oee: buffer.reduce((sum, d) => sum + d.oee, 0) / buffer.length,\n availability: buffer.reduce((sum, d) => sum + d.availability, 0) / buffer.length,\n performance: buffer.reduce((sum, d) => sum + d.performance, 0) / buffer.length,\n quality: buffer.reduce((sum, d) => sum + d.quality, 0) / buffer.length\n};\n\nnode.warn(`[KPI History] Recording averaged KPIs from ${buffer.length} samples: OEE=${avg.oee.toFixed(1)}%`);\n\n// ========== RECORD TO HISTORY ==========\n// Load history arrays\nlet oeeHist = state.realOEE || [];\nlet availHist = state.realAvailability || [];\nlet perfHist = state.realPerformance || [];\nlet qualHist = state.realQuality || [];\n\n// Append averaged values\noeeHist.push({ tsMs: now, value: Math.round(avg.oee * 10) / 10 });\navailHist.push({ tsMs: now, value: Math.round(avg.availability * 10) / 10 });\nperfHist.push({ tsMs: now, value: Math.round(avg.performance * 10) / 10 });\nqualHist.push({ tsMs: now, value: Math.round(avg.quality * 10) / 10 });\n\n// Trim arrays (avoid memory explosion)\noeeHist = oeeHist.slice(-300);\navailHist = availHist.slice(-300);\nperfHist = perfHist.slice(-300);\nqualHist = qualHist.slice(-300);\n\n// Save\nstate.realOEE = oeeHist;\nstate.realAvailability = availHist;\nstate.realPerformance = perfHist;\nstate.realQuality = qualHist;\n\n// Update state\nstate.lastKPIRecordTime = now;\nstate.kpiBuffer = []; // Clear buffer\nsaveState();\n\n// Send to graphs\nreturn {\n topic: \"chartsData\",\n payload: {\n oee: oeeHist,\n availability: availHist,\n performance: perfHist,\n quality: qualHist\n }\n};\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1750,"y":660,"wires":[["ca6e4da8d0c5f853"]]},{"id":"2139786b0733335d","type":"inject","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"Init on Deploy","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":380,"y":220,"wires":[["59c4e2cd83fd1876"]]},{"id":"59c4e2cd83fd1876","type":"function","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"Initialize Global Variables","func":"node.warn(\"[INIT] Initializing global variables\");\n\nconst settings = global.get(\"settings\") || {};\nconst state = global.get(\"state\") || {};\n\nlet fileSettings = null;\ntry {\n fileSettings = global.get(\"settings\", \"file\");\n} catch (err) {\n fileSettings = null;\n}\nif (fileSettings && typeof fileSettings === \"object\") {\n if (settings.moldTotal == null && fileSettings.moldTotal != null) {\n settings.moldTotal = fileSettings.moldTotal;\n }\n if (settings.moldActive == null && fileSettings.moldActive != null) {\n settings.moldActive = fileSettings.moldActive;\n }\n if (!Array.isArray(settings.shifts) && Array.isArray(fileSettings.shifts)) {\n settings.shifts = fileSettings.shifts;\n }\n if (settings.shiftChangeCompensation == null && fileSettings.shiftChangeCompensation != null) {\n settings.shiftChangeCompensation = fileSettings.shiftChangeCompensation;\n }\n if (settings.lunchBreakMinutes == null && fileSettings.lunchBreakMinutes != null) {\n settings.lunchBreakMinutes = fileSettings.lunchBreakMinutes;\n }\n if (settings.thresholdMultiplier == null && fileSettings.thresholdMultiplier != null) {\n settings.thresholdMultiplier = fileSettings.thresholdMultiplier;\n }\n}\n\nlet moldCache = null;\ntry {\n moldCache = global.get(\"moldCache\", \"file\");\n} catch (err) {\n moldCache = null;\n}\nif (moldCache && typeof moldCache === \"object\") {\n if (!Number.isFinite(state.lastMoldActive) && Number.isFinite(moldCache.lastMoldActive)) {\n state.lastMoldActive = moldCache.lastMoldActive;\n }\n if (!Number.isFinite(state.lastMoldTotal) && Number.isFinite(moldCache.lastMoldTotal)) {\n state.lastMoldTotal = moldCache.lastMoldTotal;\n }\n if (!state.moldByWorkOrder && moldCache.moldByWorkOrder && typeof moldCache.moldByWorkOrder === \"object\") {\n state.moldByWorkOrder = moldCache.moldByWorkOrder;\n }\n}\n\n// KPI Buffer for averaging\nif (!Array.isArray(state.kpiBuffer)) {\n state.kpiBuffer = [];\n node.warn(\"[INIT] Set state.kpiBuffer to []\");\n}\n\n// Last KPI record time - set to 1 min ago for immediate first record\nif (typeof state.lastKPIRecordTime !== \"number\") {\n state.lastKPIRecordTime = Date.now() - 60000;\n node.warn(\"[INIT] Set state.lastKPIRecordTime\");\n}\n\n// Last machine cycle time - set to now to prevent immediate 0% availability\nif (typeof state.lastMachineCycleTime !== \"number\") {\n state.lastMachineCycleTime = Date.now();\n node.warn(\"[INIT] Set state.lastMachineCycleTime to prevent 0% availability on startup\");\n}\n\n// Last KPI values\nif (!state.lastKPIValues || typeof state.lastKPIValues !== \"object\") {\n state.lastKPIValues = {};\n node.warn(\"[INIT] Set state.lastKPIValues to {}\");\n}\n\n// KPI Startup Mode - ensure clean state on deploy\nstate.kpiStartupMode = false;\nnode.warn(\"[INIT] Set state.kpiStartupMode to false\");\n\n// Tracking flags - ensure clean state\nif (typeof state.trackingEnabled !== \"boolean\") {\n state.trackingEnabled = false;\n}\nif (typeof state.productionStarted !== \"boolean\") {\n state.productionStarted = false;\n}\n\n// Settings defaults\nif (!Array.isArray(settings.shifts) || settings.shifts.length === 0) {\n settings.shifts = [{ start: \"06:00\", end: \"15:00\" }];\n node.warn(\"[INIT] Set default shift: 06:00-15:00\");\n}\n\nif (typeof settings.shiftChangeCompensation !== \"number\") {\n settings.shiftChangeCompensation = 10;\n}\n\nif (typeof settings.lunchBreakMinutes !== \"number\") {\n settings.lunchBreakMinutes = 30;\n}\n\nif (typeof settings.thresholdMultiplier !== \"number\") {\n settings.thresholdMultiplier = 1.5;\n}\n\n// State defaults\nif (typeof state.operatingTime !== \"number\") {\n state.operatingTime = 0;\n}\n\nif (typeof state.stopTime !== \"number\") {\n state.stopTime = 0;\n}\n\nif (typeof state.plannedProductionTime !== \"number\") {\n state.plannedProductionTime = 0;\n}\n\nif (!Object.prototype.hasOwnProperty.call(state, \"lastCycleCompletionTime\")) {\n state.lastCycleCompletionTime = null;\n}\n\nglobal.set(\"settings\", settings);\ntry {\n global.set(\"settings\", settings, \"file\");\n} catch (err) {\n // ignore if file store is not configured\n}\nglobal.set(\"state\", state);\nconst moldCacheOut = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n};\nglobal.set(\"moldCache\", moldCacheOut);\ntry {\n global.set(\"moldCache\", moldCacheOut, \"file\");\n} catch (err) {\n // ignore if file store is not configured\n}\n\nnode.warn(\"[INIT] Global variable initialization complete\");\n\n// Trigger restore-session to check for running work orders\nconst restoreMsg = { action: \"restore-session\" };\nreturn [null, restoreMsg];","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":630,"y":220,"wires":[[],["c7a8bb7adab6838e"]]},{"id":"30be157b60fc55fd","type":"switch","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"DB Guard (Cycles)","property":"topic","propertyType":"msg","rules":[{"t":"istype","v":"string","vt":"string"}],"checkall":"true","repair":false,"outputs":1,"x":1690,"y":380,"wires":[["037e4825e2537ed2"]]},{"id":"52501193c1677944","type":"link out","z":"8ccf34b55a2afcad","g":"8b73333804b30ee0","name":"link out 10","mode":"link","links":["76598272c6e67323"],"x":1955,"y":80,"wires":[]},{"id":"4bb96d61f9e3bba2","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Progress Check Handler","func":"// Handle DB result from start-work-order progress check\nconst state = global.get(\"state\") || {};\nconst persistMoldCache = () => {\n const cache = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n };\n global.set(\"moldCache\", cache);\n try {\n global.set(\"moldCache\", cache, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst persistState = () => {\n global.set(\"state\", state);\n};\n\nif (msg._mode === \"start-check-progress\") {\n const order = flow.get(\"pendingWorkOrder\");\n\n if (!order || !order.id) {\n node.error(\"No pending work order found\", msg);\n return [null, null];\n }\n\n // Get progress from DB query result\n const dbRow = (Array.isArray(msg.payload) && msg.payload.length > 0) ? msg.payload[0] : null;\n const cycleCount = dbRow ? (Number(dbRow.cycle_count) || 0) : 0;\n const goodParts = dbRow ? (Number(dbRow.good_parts) || 0) : 0;\n const scrapParts = dbRow ? (Number(dbRow.scrap_parts) || 0) : 0;\n const targetQty = dbRow ? (Number(dbRow.target_qty) || 0) : (Number(order.target) || 0);\n const cavitiesTotal = dbRow ? (Number(dbRow.cavities_total) || 0) : 0;\n const cavitiesActive = dbRow ? (Number(dbRow.cavities_active) || 0) : 0;\n\n const hasProgress = cycleCount > 0 || (goodParts + scrapParts) > 0;\n state.activeOrderHasProgress = hasProgress;\n state.activeOrderId = order.id;\n\n\n node.warn(`[PROGRESS-CHECK] WO ${order.id}: cycles=${cycleCount}, good=${goodParts}, target=${targetQty}`);\n\n // Check if work order has existing progress\n if (hasProgress) {\n // Work order has progress - send prompt to UI\n node.warn(`[PROGRESS-CHECK] Work order has existing progress - sending prompt to UI`);\n\n if (Number.isFinite(cavitiesActive) && cavitiesActive > 0) {\n order.cavities = cavitiesActive;\n }\n if (Number.isFinite(cavitiesTotal) && cavitiesTotal > 0) {\n order.cavities_total = cavitiesTotal;\n }\n\n const promptMsg = {\n _mode: \"resume-prompt\",\n topic: \"resumePrompt\",\n payload: {\n id: order.id,\n sku: order.sku || \"\",\n cycleCount: cycleCount,\n goodParts: goodParts,\n targetQty: targetQty,\n progressPercent: targetQty > 0 ? Math.round((goodParts / targetQty) * 100) : 0,\n // Include full order object for resume/restart actions\n order: { ...order, cycleCount: cycleCount, goodParts: goodParts, scrapParts: scrapParts }\n }\n };\n\n persistState();\n persistMoldCache();\n return [null, promptMsg];\n } else {\n // No existing progress - proceed with normal start\n // But still use DB values (even if 0) to ensure DB is source of truth\n node.warn(`[PROGRESS-CHECK] No existing progress - proceeding with normal start`);\n\n state.activeOrderHasProgress = false;\n\n // Update order object with DB values (makes DB the source of truth)\n order.cycleCount = cycleCount; // Will be 0 from DB\n order.goodParts = goodParts; // Will be 0 from DB\n order.scrapParts = scrapParts; // Will be 0 from DB\n order.target = targetQty; // From DB\n\n const moldByWorkOrder = state.moldByWorkOrder || {};\n const mapped = moldByWorkOrder[order.id] || {};\n const resolvedCavities = Number(\n order.cavities ?? cavitiesActive ?? mapped.active ?? state.lastMoldActive ?? 0\n );\n const resolvedTotal = Number(\n order.cavities_total ?? cavitiesTotal ?? mapped.total ?? state.lastMoldTotal ?? 0\n );\n if (Number.isFinite(resolvedCavities) && resolvedCavities > 0) {\n order.cavities = resolvedCavities;\n state.lastMoldActive = resolvedCavities;\n }\n if (Number.isFinite(resolvedTotal) && resolvedTotal > 0) {\n order.cavities_total = resolvedTotal;\n state.lastMoldTotal = resolvedTotal;\n }\n if (Number.isFinite(resolvedCavities) && resolvedCavities > 0) {\n moldByWorkOrder[order.id] = {\n total: Number.isFinite(resolvedTotal) && resolvedTotal > 0 ? resolvedTotal : (state.lastMoldTotal ?? null),\n active: resolvedCavities\n };\n state.moldByWorkOrder = moldByWorkOrder;\n }\n\n const startMsg = {\n _mode: \"start\",\n startOrder: order,\n topic: \"UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END, cavities_total = COALESCE(NULLIF(?,0), cavities_total), cavities_active = COALESCE(NULLIF(?,0), cavities_active) WHERE status <> 'DONE'\",\n payload: [order.id, order.id, Number(order.cavities_total || 0), Number(order.cavities || 0)]\n };\n\n // Initialize global state with DB values (even if 0)\n state.activeWorkOrder = order;\n persistState();\n state.cycleCount = cycleCount;\n persistState(); // Use DB value instead of hardcoded 0\n flow.set(\"lastMachineState\", 0);\n state.scrapPromptIssuedFor = null;\n persistState();\n persistMoldCache();\n\n node.warn(`[PROGRESS-CHECK] Initialized from DB: cycles=${cycleCount}, good=${goodParts}, scrap=${scrapParts}`);\n\n return [startMsg, null];\n }\n}\n\n// Pass through all other messages\nreturn [msg, null];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1910,"y":540,"wires":[["97399876ca829d1a"],["2872629594daf0ed"]]},{"id":"ea175b8208d85ed5","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Merge Cycle + KPI Data","func":"// ============================================================\n// DATA MERGER - Combines Cycle + KPI data for Anomaly Detector\n// ============================================================\n\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\n\n// Get KPIs from incoming message (from Calculate KPIs node)\nconst kpis = msg.kpis || msg.payload?.kpis || {};\n\n// Get cycle data from global context\nconst activeOrder = state.activeWorkOrder || {};\nconst cycleCount = state.cycleCount || 0;\nconst moldByWorkOrder = state.moldByWorkOrder || {};\nconst cavities = Number(\n activeOrder.cavities ??\n moldByWorkOrder[activeOrder.id]?.active ??\n state.lastMoldActive ??\n settings.moldActive ??\n 0\n);\nconst lastActualCycleTime = Number(state.lastActualCycleTime || 0);\n\n\n// Build cycle object with all necessary data\nconst cycle = {\n id: activeOrder.id,\n sku: activeOrder.sku || \"\",\n cycles: cycleCount,\n goodParts: Number(activeOrder.goodParts) || 0,\n scrapParts: Number(activeOrder.scrapParts) || 0,\n target: Number(activeOrder.target) || 0,\n cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0),\n progressPercent: Number(activeOrder.progressPercent) || 0,\n cavities: cavities,\n actualCycleTime: lastActualCycleTime\n\n};\n\n// Merge both into the message\nmsg.cycle = cycle;\nmsg.kpis = kpis;\n\n//node.warn(`[DATA MERGER] Merged cycle (count: ${cycleCount}) + KPIs (OEE: ${kpis.oee || 0}%) for anomaly detection`);\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1330,"y":480,"wires":[["04accd50a0fe3c90"]]},{"id":"04accd50a0fe3c90","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Anomaly Detector","func":"\n// ============================================================\n// ANOMALY DETECTOR - FIXED VERSION\n// Key fixes:\n// 1. Removed duplicate suppression that blocks microstop alerts\n// 2. Added periodic updates for active macrostops\n// 3. Improved logging for debugging\n// ============================================================\n\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\nconst anomaly = global.get(\"anomaly\") || {};\n\n// Suppress anomaly detection during mold change\nif (state.moldChange && state.moldChange.active) return null;\n\nconst cycle = msg.cycle || {};\nconst kpis = msg.kpis || {};\nconst activeOrder = state.activeWorkOrder || {};\nconst cycleCountNow = Number(cycle.cycles || 0);\n\n\n// Must have active work order to detect anomalies\nif (!activeOrder.id) {\n return null;\n}\n\nconst theoreticalCycleTime = Number(activeOrder.cycleTime) || 0;\nconst now = Date.now();\n\n// Get or initialize anomaly tracking state\nlet anomalyState = global.get(\"anomalyState\") || {\n lastCycleTime: Number(state.lastMachineCycleTime || now),\n //lastCycleCount: cycleCountNow,\n lastCycleCount: 0,\n activeStoppageEvent: null,\n oeeHistory: [],\n performanceHistory: [],\n qualityHistory: [],\n activeOeeDrop: false,\n oeeLowStreak: 0,\n activeQualitySpike: false,\n qualityHighStreak: 0,\n lastSlowCycleCount: 0,\n lastCycleEventCount: 0,\n lastStoppageUpdateMs: 0\n};\n// Reset anomaly cycle baseline when work order changes\nif (anomalyState.work_order_id && anomalyState.work_order_id !== activeOrder.id) {\n node.warn(`[RESET] Work order changed ${anomalyState.work_order_id} -> ${activeOrder.id}. Resetting anomalyState counters.`);\n anomalyState.lastCycleCount = 0;\n anomalyState.lastCycleEventCount = 0;\n anomalyState.lastSlowCycleCount = 0;\n anomalyState.activeStoppageEvent = null;\n anomalyState.lastStoppageUpdateMs = 0;\n}\nanomalyState.work_order_id = activeOrder.id;\n\nif (!isFinite(anomalyState.lastCycleTime)) {\n anomalyState.lastCycleTime = Number(state.lastMachineCycleTime || now);\n}\nif (!isFinite(anomalyState.lastCycleCount)) {\n anomalyState.lastCycleCount = cycleCountNow;\n}\n\nconst stateLastCycleTime = Number(state.lastMachineCycleTime || 0);\n//node.warn(`[TS CHECK] state.lastMachineCycleTime=${stateLastCycleTime} iso=${stateLastCycleTime ? new Date(stateLastCycleTime).toISOString() : 'n/a'}`);\nlet lastCycleTime = Number(anomalyState.lastCycleTime || 0);\nif (stateLastCycleTime > 0 && (lastCycleTime <= 0 || stateLastCycleTime > lastCycleTime)) {\n lastCycleTime = stateLastCycleTime;\n}\nconst prevCount = Number(anomalyState.lastCycleCount || 0);\n\n// If PLC/logic reset the counter (or new batch), re-baseline\nif (cycleCountNow > 0 && prevCount > 0 && cycleCountNow < prevCount) {\n node.warn(`[RESET] cycle counter reset detected: ${prevCount} -> ${cycleCountNow}. Re-baselining.`);\n anomalyState.lastCycleCount = cycleCountNow;\n anomalyState.lastCycleTime = stateLastCycleTime > 0 ? stateLastCycleTime : now;\n anomalyState.lastCycleEventCount = cycleCountNow;\n anomalyState.lastSlowCycleCount = cycleCountNow;\n anomalyState.activeStoppageEvent = null;\n anomalyState.lastStoppageUpdateMs = 0;\n}\nconst hasNewCycle = cycleCountNow > Number(anomalyState.lastCycleCount || 0);\n//node.warn(`[CYCLE CHECK PRE] cycleCountNow: ${cycleCountNow}, lastCycleCount: ${anomalyState.lastCycleCount}, hasNewCycle: ${cycleCountNow > Number(anomalyState.lastCycleCount || 0)}`);\n//node.warn(`[TS CHECK] stateLastCycleTime=${stateLastCycleTime} iso=${stateLastCycleTime ? new Date(stateLastCycleTime).toISOString() : 'n/a'}`);\nif (hasNewCycle) {\n anomalyState.lastCycleCount = cycleCountNow;\n anomalyState.lastCycleTime = stateLastCycleTime > 0 ? stateLastCycleTime : now;\n lastCycleTime = anomalyState.lastCycleTime;\n}\n//node.warn(`[CYCLE CHECK] cycleCountNow: ${cycleCountNow}, lastCycleCount: ${anomalyState.lastCycleCount}, hasNewCycle: ${hasNewCycle}`);\n//node.warn(`[CYCLE CHECK] msg.cycle.cycles: ${msg.cycle.cycles}, type: ${typeof msg.cycle.cycles}`);\n// Configuration\nconst OEE_THRESHOLD = settings.oeeAlertThreshold || 90;\nconst HISTORY_WINDOW = 20;\nconst QUALITY_SPIKE_THRESHOLD = 5;\nconst PERFORMANCE_THRESHOLD = settings.performanceThreshold || 85;\nconst microMultiplier = Number(settings.thresholdMultiplier || 1.5);\nconst macroMultiplier = Math.max(\n microMultiplier,\n Number(settings.macroStoppageMultiplier || 5)\n);\n\n// NEW: Update interval for macrostop notifications (default 10 seconds)\nconst updateIntervalMs = Number(settings.stoppageUpdateIntervalMs || 10000);\n\nconst detectedAnomalies = [];\n\nconst DEFAULT_DOWNTIME_REASON = {\n type: \"downtime\",\n categoryId: \"sin-clasificar\",\n categoryLabel: \"Sin clasificar\",\n detailId: \"pendiente\",\n detailLabel: \"Pendiente de clasificar\",\n reasonCode: \"PENDIENTE\",\n reasonText: \"Pendiente de clasificar\"\n};\n\nfunction applyDefaultDowntimeReason(event) {\n if (!event || (event.anomaly_type !== \"microstop\" && event.anomaly_type !== \"macrostop\")) {\n return event;\n }\n if (event.reason && typeof event.reason === \"object\") {\n return event;\n }\n return {\n ...event,\n reason: { ...DEFAULT_DOWNTIME_REASON }\n };\n}\n\n// ============================================================\n// TIER 1: CYCLE CLASSIFICATION + STOPPAGE WATCHDOG\n// ============================================================\nif (theoreticalCycleTime > 0) {\n const actualCycleTime = Number(cycle.actualCycleTime || 0);\n const currentCycleCount = Number(cycle.cycles || 0);\n const lastCycleEventCount = Number(\n anomalyState.lastCycleEventCount ?? anomalyState.lastSlowCycleCount ?? 0\n );\n\n const microThresholdSec = theoreticalCycleTime * microMultiplier;\n const macroThresholdSec = theoreticalCycleTime * macroMultiplier;\n const timeSinceLastCycleSec = lastCycleTime > 0 ? (now - lastCycleTime) / 1000 : 0;\n\n // DEBUG LOGGING\n // node.warn(`[DEBUG] actualCycleTime: ${actualCycleTime}s, theoretical: ${theoreticalCycleTime}s`);\n // node.warn(`[DEBUG] microThreshold: ${microThresholdSec}s, macroThreshold: ${macroThresholdSec}s`);\n // node.warn(`[DEBUG] timeSinceLastCycle: ${timeSinceLastCycleSec}s, hasNewCycle: ${hasNewCycle}`);\n\n let resolvedStoppageThisCycle = false;\n\n // Resolve any active stoppage when a new cycle arrives\n if (anomalyState.activeStoppageEvent && hasNewCycle) {\n const resolvedDurationSec = actualCycleTime > 0 ? actualCycleTime : timeSinceLastCycleSec;\n\n detectedAnomalies.push({\n ...anomalyState.activeStoppageEvent,\n status: \"resolved\",\n resolved_at: now,\n auto_resolved: true,\n data: {\n ...anomalyState.activeStoppageEvent.data,\n stoppage_duration_seconds: Math.round(resolvedDurationSec)\n },\n tsMs: now\n });\n\n node.warn(`[RESOLVED] Stoppage resolved: ${anomalyState.activeStoppageEvent.anomaly_type}, duration: ${resolvedDurationSec}s`);\n anomalyState.activeStoppageEvent = null;\n anomalyState.lastStoppageUpdateMs = 0;\n resolvedStoppageThisCycle = true;\n }\n\n if (hasNewCycle) {\n //node.warn(`[CYCLE RESUME] New cycle detected! Count: ${cycleCountNow}, lastCount: ${anomalyState.lastCycleCount}`);\n //node.warn(`[CYCLE RESUME] lastCycleTime updated from ${lastCycleTime} to ${anomalyState.lastCycleTime}`);\n //node.warn(`[CYCLE RESUME] activeStoppageEvent cleared: ${anomalyState.activeStoppageEvent === null}`);\n }\n\n // Add before the periodic update section (around line ~270)\n // if (anomalyState.activeStoppageEvent) {\n // node.warn(`[WATCHDOG] Active stoppage exists: ${anomalyState.activeStoppageEvent.anomaly_type}`);\n //node.warn(`[WATCHDOG] timeSinceLastCycle: ${timeSinceLastCycleSec}s, lastCycleTime: ${lastCycleTime}, now: ${now}`);\n // }\n\n // Per-cycle classification (uses actualCycleTime)\n if (actualCycleTime > 0 && currentCycleCount > lastCycleEventCount) {\n let cycleEvent = null;\n\n const SLOW_MARGIN = Number(settings.slowCycleMarginPercent ?? 5); // 5% default\n\n if (\n actualCycleTime > 0 &&\n actualCycleTime > theoreticalCycleTime * (1 + SLOW_MARGIN / 100) &&\n actualCycleTime < microThresholdSec\n ) {\n const deltaPercent = ((actualCycleTime - theoreticalCycleTime) / theoreticalCycleTime) * 100;\n\n cycleEvent = {\n anomaly_type: \"slow-cycle\",\n severity: \"warning\",\n requires_ack: false,\n title: \"Slow Cycle Detected\",\n description: `Cycle took ${actualCycleTime.toFixed(1)}s (+${deltaPercent.toFixed(0)}% vs ${theoreticalCycleTime.toFixed(1)}s target)`,\n data: {\n actual_cycle_time: actualCycleTime,\n theoretical_cycle_time: theoreticalCycleTime,\n delta_percent: Math.round(deltaPercent),\n micro_threshold_multiplier: microMultiplier,\n macro_threshold_multiplier: macroMultiplier\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: currentCycleCount,\n tsMs: now\n };\n\n //node.warn(`[SLOW-CYCLE] ${actualCycleTime.toFixed(1)}s (expected ${theoreticalCycleTime}s)`);\n\n } /*else if (actualCycleTime >= macroThresholdSec) {\n cycleEvent = {\n anomaly_type: \"macrostop\",\n severity: \"critical\",\n requires_ack: true,\n title: \"Macrostop Detected\",\n description: `Cycle gap ${actualCycleTime.toFixed(1)}s (threshold: ${macroThresholdSec.toFixed(1)}s)`,\n data: {\n stoppage_duration_seconds: Math.round(actualCycleTime),\n theoretical_cycle_time: theoreticalCycleTime,\n micro_threshold_multiplier: microMultiplier,\n macro_threshold_multiplier: macroMultiplier\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: currentCycleCount,\n tsMs: now,\n status: \"resolved\" // Changed to resolved since cycle completed\n };\n\n node.warn(`[MACROSTOP] ${actualCycleTime.toFixed(1)}s gap detected`);\n }*/\n\n // FIXED: Always push cycle events (removed duplicate suppression)\n if (cycleEvent) {\n detectedAnomalies.push(cycleEvent);\n //node.warn(`[CYCLE EVENT] Added ${cycleEvent.anomaly_type} to detectedAnomalies`);\n }\n\n anomalyState.lastCycleEventCount = currentCycleCount;\n anomalyState.lastSlowCycleCount = currentCycleCount;\n }\n\n // No new cycle yet: start or escalate stoppage\n if (!hasNewCycle && lastCycleTime > 0) {\n // Start stoppage once when we cross micro threshold\n if (!anomalyState.activeStoppageEvent && timeSinceLastCycleSec >= microThresholdSec) {\n const isMacro = timeSinceLastCycleSec >= macroThresholdSec;\n const anomalyType = isMacro ? \"macrostop\" : \"microstop\";\n const severity = isMacro ? \"critical\" : \"warning\";\n\n const stoppageEvent = {\n alert_id: `${anomalyType}:${activeOrder.id}:${lastCycleTime}`,\n anomaly_type: anomalyType,\n severity,\n requires_ack: true,\n title: isMacro ? \"Macrostop In Progress\" : \"Microstop In Progress\",\n description: `No cycles for ${timeSinceLastCycleSec.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,\n data: {\n stoppage_duration_seconds: Math.round(timeSinceLastCycleSec),\n theoretical_cycle_time: theoreticalCycleTime,\n last_cycle_timestamp: lastCycleTime,\n micro_threshold_multiplier: microMultiplier,\n macro_threshold_multiplier: macroMultiplier\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: \"active\"\n };\n\n detectedAnomalies.push(stoppageEvent);\n anomalyState.activeStoppageEvent = stoppageEvent;\n anomalyState.lastStoppageUpdateMs = now;\n //node.warn(`[STOPPAGE START] ${anomalyType} started at ${timeSinceLastCycleSec.toFixed(0)}s`);\n }\n\n // Escalate micro -> macro once\n if (\n anomalyState.activeStoppageEvent &&\n anomalyState.activeStoppageEvent.anomaly_type === \"microstop\" &&\n timeSinceLastCycleSec >= macroThresholdSec\n ) {\n detectedAnomalies.push({\n ...anomalyState.activeStoppageEvent,\n status: \"resolved\",\n resolved_at: now,\n auto_resolved: true,\n requires_ack: false,\n data: {\n ...anomalyState.activeStoppageEvent.data,\n stoppage_duration_seconds: Math.round(timeSinceLastCycleSec)\n },\n tsMs: now\n });\n\n const macroEvent = {\n alert_id: `macrostop:${activeOrder.id}:${lastCycleTime}`,\n anomaly_type: \"macrostop\",\n severity: \"critical\",\n requires_ack: true,\n title: \"Macrostop In Progress\",\n description: `No cycles for ${timeSinceLastCycleSec.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,\n data: {\n stoppage_duration_seconds: Math.round(timeSinceLastCycleSec),\n theoretical_cycle_time: theoreticalCycleTime,\n last_cycle_timestamp: lastCycleTime,\n micro_threshold_multiplier: microMultiplier,\n macro_threshold_multiplier: macroMultiplier\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: \"active\"\n };\n\n detectedAnomalies.push(macroEvent);\n anomalyState.activeStoppageEvent = macroEvent;\n anomalyState.lastStoppageUpdateMs = now;\n //node.warn(`[ESCALATION] Microstop -> Macrostop at ${timeSinceLastCycleSec.toFixed(0)}s`);\n }\n\n // NEW: Send periodic updates for active macrostops\n if (\n anomalyState.activeStoppageEvent &&\n anomalyState.activeStoppageEvent.anomaly_type === \"macrostop\"\n ) {\n const timeSinceLastUpdate = now - (anomalyState.lastStoppageUpdateMs || 0);\n \n if (timeSinceLastUpdate >= updateIntervalMs) {\n const prev = anomalyState.activeStoppageEvent;\n\n // 1) AUTO-ACK the previous active macrostop alert\n const autoAck = {\n ...prev,\n status: \"resolved\",\n resolved_at: now,\n auto_resolved: true,\n requires_ack: false,\n title: \"Macrostop Updated\",\n description: \"Auto-acknowledged due to periodic refresh\",\n tsMs: now,\n is_auto_ack: true\n };\n\n // 2) Send a NEW active macrostop alert with updated duration + NEW alert_id\n const refreshed = {\n ...prev,\n alert_id: `macrostop:${activeOrder.id}:${now}`, // new instance id so UI treats it as new\n status: \"active\",\n requires_ack: true,\n title: \"Macrostop Ongoing\",\n description: `Machine still stopped for ${timeSinceLastCycleSec.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,\n data: {\n ...prev.data,\n stoppage_duration_seconds: Math.round(timeSinceLastCycleSec)\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n tsMs: now,\n is_update: true\n };\n\n detectedAnomalies.push(autoAck, refreshed);\n\n // IMPORTANT: set the active event to the refreshed one\n anomalyState.activeStoppageEvent = refreshed;\n anomalyState.lastStoppageUpdateMs = now;\n\n //node.warn(`[MACROSTOP REFRESH] Auto-acked previous, new duration: ${timeSinceLastCycleSec.toFixed(0)}s`);\n }\n }\n }\n}\n\n\n\n// ============================================================\n// TIER 2: OEE DROP DETECTION\n// Trigger: OEE falls below threshold\n// ============================================================\nconst currentOEE = Number(kpis.oee) || 0;\n\nif (currentOEE > 0) {\n const lowThreshold = OEE_THRESHOLD; // e.g. 90\n const recoveryThreshold = OEE_THRESHOLD + 2; // some hysteresis\n\n if (currentOEE < lowThreshold) {\n // Count consecutive low points\n anomalyState.oeeLowStreak = (anomalyState.oeeLowStreak || 0) + 1;\n\n const REQUIRED_STREAK = 3; // only alert after 3 bad readings\n\n // Only fire when we ENTER the bad zone\n if (!anomalyState.activeOeeDrop &&\n anomalyState.oeeLowStreak >= REQUIRED_STREAK) {\n\n let severity = 'warning';\n if (currentOEE < 75) severity = 'critical';\n\n detectedAnomalies.push({\n anomaly_type: 'oee-drop',\n severity,\n title: 'OEE Below Threshold',\n description: `OEE at ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`,\n data: {\n current_oee: currentOEE,\n threshold: OEE_THRESHOLD,\n delta: OEE_THRESHOLD - currentOEE\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: 'active'\n });\n\n anomalyState.activeOeeDrop = true;\n //node.warn(`[ANOMALY] OEE drop started at ${currentOEE.toFixed(1)}%`);\n }\n\n } else if (currentOEE >= recoveryThreshold) {\n // We are OUT of the bad zone\n anomalyState.oeeLowStreak = 0;\n\n if (anomalyState.activeOeeDrop) {\n detectedAnomalies.push({\n anomaly_type: 'oee-drop',\n severity: 'info',\n title: 'OEE Recovered',\n description: `OEE recovered to ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`,\n data: {\n current_oee: currentOEE,\n threshold: OEE_THRESHOLD\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: 'resolved'\n });\n\n anomalyState.activeOeeDrop = false;\n //node.warn(`[ANOMALY] OEE recovered to ${currentOEE.toFixed(1)}%`);\n }\n }\n}\n\n// Update OEE history for trend analysis\nanomalyState.oeeHistory.push({ tsMs: now, value: currentOEE });\nif (anomalyState.oeeHistory.length > HISTORY_WINDOW) {\n anomalyState.oeeHistory.shift(); // Keep only recent history\n}\n\n// ============================================================\n// TIER 2: QUALITY SPIKE DETECTION\n// Trigger: Sudden increase in scrap/defect rate\n// ============================================================\nconst totalParts = (cycle.goodParts || 0) + (cycle.scrapParts || 0);\nconst currentScrapRate =\n totalParts > 0 ? ((cycle.scrapParts || 0) / totalParts) * 100 : 0;\n\n// Keep history for trend\nanomalyState.qualityHistory.push({ tsMs: now, value: currentScrapRate });\nif (anomalyState.qualityHistory.length > HISTORY_WINDOW) {\n anomalyState.qualityHistory.shift();\n}\n\n// Only evaluate when we have enough data and enough volume in this cycle\nconst MIN_SAMPLES = 5;\nconst MIN_PARTS_THIS_CYCLE = 10;\n\nif (\n anomalyState.qualityHistory.length >= MIN_SAMPLES &&\n totalParts >= MIN_PARTS_THIS_CYCLE\n) {\n const recentHistory = anomalyState.qualityHistory.slice(0, -1); // exclude current\n const avgScrapRate =\n recentHistory.reduce((sum, p) => sum + p.value, 0) /\n recentHistory.length;\n\n const scrapRateIncrease = currentScrapRate - avgScrapRate;\n\n const SPIKE_DELTA = QUALITY_SPIKE_THRESHOLD || 5; // your existing config\n const MIN_SCRAP_RATE = 5; // ignore small scrap percentages\n const RECOVERY_MARGIN = 2; // how far back towards avg to consider \"recovered\"\n\n // ----- When scrap is HIGH / SPIKE ZONE -----\n if (\n currentScrapRate > MIN_SCRAP_RATE &&\n scrapRateIncrease > SPIKE_DELTA\n ) {\n // count how many consecutive \"bad\" cycles\n anomalyState.qualityHighStreak =\n (anomalyState.qualityHighStreak || 0) + 1;\n\n const REQUIRED_STREAK = 2; // require 2 consecutive bad cycles\n\n // fire ONLY when we ENTER the spike (not every cycle)\n if (\n !anomalyState.activeQualitySpike &&\n anomalyState.qualityHighStreak >= REQUIRED_STREAK\n ) {\n let severity = \"warning\";\n if (scrapRateIncrease > 10 || currentScrapRate > 15) {\n severity = \"critical\";\n }\n\n detectedAnomalies.push({\n anomaly_type: \"quality-spike\",\n severity,\n title: \"Quality Issue Detected\",\n description: `Scrap rate at ${currentScrapRate.toFixed(\n 1\n )}% (avg: ${avgScrapRate.toFixed(\n 1\n )}%, +${scrapRateIncrease.toFixed(1)}%)`,\n data: {\n current_scrap_rate: currentScrapRate,\n average_scrap_rate: avgScrapRate,\n increase: scrapRateIncrease,\n scrap_parts: cycle.scrapParts || 0,\n total_parts: totalParts\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: \"active\"\n });\n\n anomalyState.activeQualitySpike = true;\n /*node.warn(\n `[ANOMALY] Quality spike started: scrap ${currentScrapRate.toFixed(\n 1\n )}% (avg ${avgScrapRate.toFixed(1)}%)`\n );*/\n }\n } else {\n // ----- When scrap is NORMAL / RECOVERY -----\n anomalyState.qualityHighStreak = 0;\n\n // if we had an active spike, send a single \"resolved\" event\n if (\n anomalyState.activeQualitySpike &&\n currentScrapRate <= avgScrapRate + RECOVERY_MARGIN\n ) {\n detectedAnomalies.push({\n anomaly_type: \"quality-spike\",\n severity: \"info\",\n title: \"Quality Issue Resolved\",\n description: `Scrap rate back to ${currentScrapRate.toFixed(\n 1\n )}% (avg: ${avgScrapRate.toFixed(1)}%)`,\n data: {\n current_scrap_rate: currentScrapRate,\n average_scrap_rate: avgScrapRate\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: \"resolved\"\n });\n\n anomalyState.activeQualitySpike = false;\n /*node.warn(\n `[ANOMALY] Quality spike resolved: scrap ${currentScrapRate.toFixed(\n 1\n )}%`\n );*/\n }\n }\n}\n\n// ============================================================\n// TIER 2: PERFORMANCE DEGRADATION\n// Trigger: Consistent underperformance over time\n// ============================================================\nconst currentPerformance = Number(kpis.performance) || 0;\nanomalyState.performanceHistory.push({ tsMs: now, value: currentPerformance });\nif (anomalyState.performanceHistory.length > HISTORY_WINDOW) {\n anomalyState.performanceHistory.shift();\n}\n\n// Check for sustained poor performance (at least 10 data points)\nif (anomalyState.performanceHistory.length >= 10) {\n const recent10 = anomalyState.performanceHistory.slice(-10);\n const avgPerformance = recent10.reduce((sum, point) => sum + point.value, 0) / recent10.length;\n\n const PERF_LOW_THRESHOLD = PERFORMANCE_THRESHOLD; // 85%\n const PERF_RECOVERY_THRESHOLD = PERFORMANCE_THRESHOLD + 3; // 88% to recover\n\n // Check if we're in degraded state\n if (avgPerformance > 0 && avgPerformance < PERF_LOW_THRESHOLD) {\n // Count consecutive low readings\n anomalyState.performanceLowStreak = (anomalyState.performanceLowStreak || 0) + 1;\n\n const REQUIRED_STREAK = 3; // Need 3 consecutive low readings\n\n // Only fire ONCE when entering degraded state\n if (!anomalyState.activePerformanceDegradation &&\n anomalyState.performanceLowStreak >= REQUIRED_STREAK) {\n\n let severity = 'warning';\n if (avgPerformance < 75) {\n severity = 'critical';\n }\n\n detectedAnomalies.push({\n anomaly_type: 'performance-degradation',\n severity: severity,\n title: `Performance Degradation`,\n description: `Performance at ${avgPerformance.toFixed(1)}% (sustained over last 10 cycles)`,\n data: {\n average_performance: avgPerformance,\n current_performance: currentPerformance,\n threshold: PERF_LOW_THRESHOLD,\n sample_size: 10\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: 'active'\n });\n\n // Mark as active so we don't spam\n anomalyState.activePerformanceDegradation = true;\n //node.warn(`[ANOMALY] Performance degradation STARTED: ${avgPerformance.toFixed(1)}%`);\n }\n\n } else if (avgPerformance >= PERF_RECOVERY_THRESHOLD) {\n // Performance recovered\n anomalyState.performanceLowStreak = 0;\n\n // Only send recovery message if we were previously in degraded state\n if (anomalyState.activePerformanceDegradation) {\n detectedAnomalies.push({\n anomaly_type: 'performance-degradation',\n severity: 'info',\n title: 'Performance Recovered',\n description: `Performance recovered to ${avgPerformance.toFixed(1)}% (threshold: ${PERF_LOW_THRESHOLD}%)`,\n data: {\n average_performance: avgPerformance,\n current_performance: currentPerformance,\n threshold: PERF_LOW_THRESHOLD\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: 'resolved'\n });\n\n anomalyState.activePerformanceDegradation = false;\n //node.warn(`[ANOMALY] Performance degradation RESOLVED: ${avgPerformance.toFixed(1)}%`);\n }\n }\n}\n\n// ============================================================\n// TIER 3: PREDICTIVE ALERTS (Trend Analysis)\n// Predict issues before they become critical\n// ============================================================\nif (anomalyState.oeeHistory.length >= 15) {\n // Simple linear trend analysis on OEE\n const recent15 = anomalyState.oeeHistory.slice(-15);\n const firstHalf = recent15.slice(0, 7);\n const secondHalf = recent15.slice(-7);\n\n const avgFirstHalf = firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length;\n const avgSecondHalf = secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length;\n\n const oeeTrend = avgSecondHalf - avgFirstHalf;\n\n // Predict if OEE is trending downward significantly\n if (oeeTrend < -5 && avgSecondHalf > OEE_THRESHOLD * 0.95 && avgSecondHalf < OEE_THRESHOLD * 1.05) {\n detectedAnomalies.push({\n anomaly_type: 'predictive-oee-decline',\n severity: 'info',\n title: `Declining OEE Trend Detected`,\n description: `OEE trending down ${Math.abs(oeeTrend).toFixed(1)}% over last 15 cycles. Current: ${avgSecondHalf.toFixed(1)}%`,\n data: {\n trend: oeeTrend,\n first_half_avg: avgFirstHalf,\n second_half_avg: avgSecondHalf,\n prediction: 'OEE may drop below threshold soon'\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now\n });\n\n //node.warn(`[PREDICTIVE] OEE trending down: ${oeeTrend.toFixed(1)}%`);\n }\n}\n\n// Update last cycle time for next iteration\nanomalyState.lastCycleTime = lastCycleTime;\nglobal.set(\"anomalyState\", anomalyState);\n//anomaly.state = anomalyState;\n//global.set(\"anomaly\", anomaly);\n\n\n// ============================================================\n// OUTPUT\n// ============================================================\nconst normalizedAnomalies = detectedAnomalies.map(applyDefaultDowntimeReason);\n\nif (normalizedAnomalies.length > 0) {\n node.warn(`[ANOMALY DETECTOR] Detected ${normalizedAnomalies.length} anomaly/ies`);\n\n normalizedAnomalies.forEach((a, i) => {\n node.warn(` [${i + 1}] ${a.anomaly_type} - ${a.title} - ${a.status || 'N/A'}`);\n });\n\n msg.topic = \"anomaly-detected\";\n msg.payload = normalizedAnomalies;\n msg.originalMsg = msg.originalMsg || null;\n msg._anomaly_source = \"anomaly_detector\";\n return msg;\n}\nreturn null;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1350,"y":440,"wires":[["4a382caf6dadab4c","e5272e3e630666ce","a1e59ef54a91ee50","254622f455de3a4c"]]},{"id":"4a382caf6dadab4c","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Event Logger (Simplified)","func":"// ============================================================\n// EVENT LOGGER - SIMPLIFIED (INSERTS ONLY)\n// Every anomaly gets inserted as a new row\n// ============================================================\n\nconst anomalies = msg.payload || [];\nconst settings = global.get(\"settings\") || {};\nconst anomalyStateStore = global.get(\"anomaly\") || {};\nconst reasonsByIncident = anomalyStateStore.reasonsByIncident || {};\n\nif (!Array.isArray(anomalies) || anomalies.length === 0) {\n return null;\n}\n\n// SQL escape helper\nconst esc = (v) => {\n if (v === null || v === undefined) return 'NULL';\n return \"'\" + String(v).replace(/\\\\/g, '\\\\').replace(/'/g, \"''\") + \"'\";\n};\n\nconst dbInserts = [];\nconst activeAnomalies = [];\nconst softNotifications = []; \n\n\nanomalies.forEach(anomaly => {\n const tsMs = Number(anomaly.tsMs) || Date.now();\n const woId = anomaly.work_order_id || '';\n const aType = anomaly.anomaly_type || 'unknown';\n const sev = anomaly.severity || 'warning';\n const title = anomaly.title || '';\n const desc = anomaly.description || '';\n const dataJson = JSON.stringify(anomaly.data || {});\n const kpiJson = JSON.stringify(anomaly.kpi_snapshot || {});\n const cycle = Number(anomaly.cycle_count) || 0;\n const requiresAck = anomaly.requires_ack !== false;\n const incidentKey = anomaly.incidentKey || (anomaly.data && anomaly.data.last_cycle_timestamp\n ? [aType, woId, String(anomaly.data.last_cycle_timestamp)].join(\":\")\n : (anomaly.alert_id || null));\n const reason = incidentKey ? (reasonsByIncident[incidentKey] || null) : null;\n\n // Build INSERT query\n const insertQuery = \n \"INSERT INTO anomaly_events \" +\n \"(`event_timestamp`, `work_order_id`, `anomaly_type`, `severity`, `title`, `description`, \" +\n \"`data_json`, `kpi_snapshot_json`, `status`, `cycle_count`, `occurrence_count`, `last_occurrence`) VALUES (\" +\n tsMs + \", \" +\n esc(woId) + \", \" +\n esc(aType) + \", \" +\n esc(sev) + \", \" +\n esc(title) + \", \" +\n esc(desc) + \", \" +\n esc(dataJson) + \", \" +\n esc(kpiJson) + \", \" +\n \"'active', \" +\n cycle + \", \" +\n \"1, \" +\n tsMs + \")\";\n\n dbInserts.push({ topic: insertQuery, payload: [] });\n\n // Add to active list for UI\n if (requiresAck) {\n // Hard alerts that go to the panel + require acknowledgment\n activeAnomalies.push({\n event_id: null,\n tsMs: tsMs,\n work_order_id: woId,\n anomaly_type: aType,\n incidentKey: incidentKey || null,\n severity: sev,\n title: title,\n description: desc,\n status: 'active',\n reason: reason,\n kpi_snapshot: anomaly.kpi_snapshot || {}\n });\n\n node.warn(`[EVENT LOGGER] Inserting ${aType}: ${title} (requires ack)`);\n } else {\n // Soft alerts (e.g. slow-cycle) -> just show a transient popup\n softNotifications.push({\n tsMs: tsMs,\n anomaly_type: aType,\n severity: sev,\n title: title,\n description: desc,\n requires_ack: false\n });\n\n node.warn(`[EVENT LOGGER] Logging soft anomaly ${aType}: ${title}`);\n }\n});\n\n// UI update message\nconst uiMsg = {\n topic: \"anomaly-ui-update\",\n payload: {\n activeCount: activeAnomalies.length,\n activeAnomalies: activeAnomalies,\n updates: activeAnomalies.map(a => ({ status: 'new', anomaly: a })),\n softNotifications: softNotifications,\n reasonCatalog: settings.reasonCatalog || null\n }\n};\n\nreturn [dbInserts, uiMsg];\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1370,"y":400,"wires":[["1e8c2370ba268726","d116553e03c9a47c","4ea28603310c2aef"],["286df6ee52e241ca","fa4a2d371b22788a"]]},{"id":"d116553e03c9a47c","type":"split","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Split DB Inserts","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":1940,"y":380,"wires":[["8dcfbe64c30024e6"]]},{"id":"2dffe9379e43af37","type":"mysql","z":"8ccf34b55a2afcad","g":"40295dcf4b5e1779","mydb":"fc9634aabefee16b","name":"Anomaly Events DB","x":1020,"y":60,"wires":[[]]},{"id":"bcc7ea4f7444b199","type":"inject","z":"8ccf34b55a2afcad","name":"Initialize OEE Threshold (90%)","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"90","payloadType":"num","x":360,"y":940,"wires":[["407a6d0315f2812d"]]},{"id":"407a6d0315f2812d","type":"function","z":"8ccf34b55a2afcad","name":"Set OEE Threshold Global","func":"// Initialize OEE alert threshold\nconst settings = global.get(\"settings\") || {};\nconst threshold = Number(msg.payload) || 90;\nsettings.oeeAlertThreshold = threshold;\nglobal.set(\"settings\", settings);\n\nnode.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`);\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":940,"wires":[[]]},{"id":"c323fd9c710bcded","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Save KPIs to Database","func":"// ============================================================\n// SAVE KPIs TO DATABASE - 1 SNAPSHOT PER CYCLE\n// ============================================================\n\nconst state = global.get(\"state\") || {};\nconst kpis = msg.kpis || {};\n\n// Rising edge guard: only save once per cycle\nconst saveFlag = state.saveKpis || 0;\nif (!saveFlag) {\n return null;\n}\nstate.saveKpis = 0;\nglobal.set(\"state\", state);\n\nconst dbInserts = [];\n\nconst activeOrder = state.activeWorkOrder || {};\nconst workorder_id = activeOrder.id;\nconst oee = Number(kpis.oee);\nconst performance = Number(kpis.performance);\nconst availability = Number(kpis.availability);\nconst quality = Number(kpis.quality);\nconst tsMs = Date.now();\n\nif (!workorder_id) {\n return null;\n}\n\nconst insertQuery =\n \"INSERT INTO kpi_snapshots \" +\n \"(work_order_id,oee_percent, performance_percent, availability_percent, quality_percent, timestamp) VALUES (\" +\n \"'\" + workorder_id + \"', \" +\n oee + \", \" +\n performance + \", \" +\n availability + \", \" +\n quality + \", \" +\n tsMs + \")\";\n\ndbInserts.push({ topic: insertQuery, payload: [] });\n\nreturn [dbInserts];\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1710,"y":720,"wires":[["8fe1661536e76e10"]]},{"id":"8fe1661536e76e10","type":"mysql","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","mydb":"fc9634aabefee16b","name":"Save kpis to database","x":1980,"y":700,"wires":[["1a14505b6501d2dc"]]},{"id":"429b86c710e30c33","type":"template","z":"8ccf34b55a2afcad","g":"8b73333804b30ee0","name":"Format query 1","field":"topic","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"SELECT\n oee_percent,\n availability_percent,\n quality_percent,\n performance_percent,\n tsMs\nFROM (\n SELECT\n oee_percent,\n availability_percent,\n quality_percent,\n performance_percent,\n timestamp AS tsMs\n FROM kpi_snapshots\n ORDER BY timestamp DESC\n LIMIT 50\n) AS t\nORDER BY tsMs ASC;\n","output":"str","x":1380,"y":80,"wires":[["2848a91e7a88cd0b"]]},{"id":"89d2d406e3cd96ee","type":"function","z":"8ccf34b55a2afcad","g":"8b73333804b30ee0","name":"Format Graph Data","func":"// Format Graph Data for KPI charts\n\n // Build labels and data arrays\n const labels = [];\n const oeeData = [];\n const availData = [];\n const perfData = [];\n const qualData = [];\n\n function bucketSeries(source, size) {\n const bucketSize = size || 5; // 3 points → 1 smoother point\n if (!Array.isArray(source) || source.length <= bucketSize) return source;\n\n const result = [];\n for (let i = 0; i < source.length; i += bucketSize) {\n const bucket = source.slice(i, i + bucketSize);\n const avgY = bucket.reduce((sum, p) => sum + Number(p.y || 0), 0) / bucket.length;\n const midPoint = bucket[Math.floor(bucket.length / 2)] || bucket[0];\n result.push({ x: midPoint.x, y: avgY });\n }\n return result;\n}\n\n \n msg.payload.forEach(row => {\n let x_value = new Date(row.tsMs); \n const dateObject = new Date(x_value);\n const year = dateObject.getFullYear();\n const month = dateObject.getMonth() + 1; // Months are 0-indexed\n const day = dateObject.getDate();\n const hours = dateObject.getHours();\n const minutes = dateObject.getMinutes();\n const seconds = dateObject.getSeconds();\n const formattedx_value = `${day.toString().padStart(2, '0')}-${month.toString().padStart(2, '0')} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n\n oeeData.push({ x: formattedx_value, y: row.oee_percent });\n availData.push({ x: formattedx_value, y: row.availability_percent });\n perfData.push({ x: formattedx_value, y: row.performance_percent });\n qualData.push({ x: formattedx_value, y: row.quality_percent });\n });\n\n const smoothOee = bucketSeries(oeeData, 5);\n const smoothAvail = bucketSeries(availData, 5);\n const smoothPerf = bucketSeries(perfData, 5);\n const smoothQual = bucketSeries(qualData, 5);\n\n msg.graphData = {\n labels: labels,\n datasets: [\n { label: 'OEE %', data: smoothOee },\n { label: 'Availability %', data: smoothAvail },\n { label: 'Performance %', data: smoothPerf },\n { label: 'Quality %', data: smoothQual }\n ]\n };\n\n //node.warn(`[GRAPH DATA] Formatted ${labels.length} KPI history points`);\n\n delete msg.topic;\n delete msg.payload;\n return msg;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1830,"y":80,"wires":[["52501193c1677944"]]},{"id":"7a1de7abad9506ec","type":"link out","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"link out 11","mode":"link","links":["154c7f1ad173419e"],"x":485,"y":360,"wires":[]},{"id":"154c7f1ad173419e","type":"link in","z":"8ccf34b55a2afcad","g":"1f2e45c551f40615","name":"link in 10","links":["7a1de7abad9506ec"],"x":275,"y":860,"wires":[["851a0e837a94ed78"]]},{"id":"286df6ee52e241ca","type":"link out","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link out 12","mode":"link","links":["184c513e8d1389dd"],"x":1685,"y":420,"wires":[]},{"id":"184c513e8d1389dd","type":"link in","z":"8ccf34b55a2afcad","g":"40295dcf4b5e1779","name":"link in 11","links":["286df6ee52e241ca"],"x":265,"y":60,"wires":[["25e40f96b0876550"]]},{"id":"8dcfbe64c30024e6","type":"link out","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link out 13","mode":"link","links":["d4ad56bfc7f333ba"],"x":2065,"y":380,"wires":[]},{"id":"d4ad56bfc7f333ba","type":"link in","z":"8ccf34b55a2afcad","g":"40295dcf4b5e1779","name":"link in 12","links":["8dcfbe64c30024e6","1e8c2370ba268726"],"x":895,"y":80,"wires":[["2dffe9379e43af37"]]},{"id":"1e8c2370ba268726","type":"link out","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link out 14","mode":"link","links":["d4ad56bfc7f333ba"],"x":2215,"y":420,"wires":[]},{"id":"c72da0d5e173ccd5","type":"link out","z":"8ccf34b55a2afcad","g":"c95c05b78d8464c5","name":"link out 15","mode":"link","links":["9d321f6ae2618e66"],"x":1505,"y":200,"wires":[]},{"id":"9d321f6ae2618e66","type":"link in","z":"8ccf34b55a2afcad","g":"8b73333804b30ee0","name":"link in 13","links":["c72da0d5e173ccd5"],"x":1235,"y":80,"wires":[["429b86c710e30c33"]]},{"id":"e5272e3e630666ce","type":"link out","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"link out 16","mode":"link","links":[],"x":1535,"y":440,"wires":[]},{"id":"f1a89a5092479152","type":"function","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","name":"Shift Config Handler","func":"// Shift Config Handler\nconst topic = msg.topic || \"\";\nconst config = global.get(\"config\") || {};\nconst readOnly = config.settingsReadOnly !== false;\nconst settings = global.get(\"settings\") || {};\n\nif (readOnly && (topic === \"saveShiftConfig\" || topic === \"saveThresholdConfig\" || topic === \"saveAllSettings\")) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Read-only\" });\n return null;\n}\n\n// Save shift config\nif (topic === \"saveShiftConfig\") {\n const config = msg.payload || {};\n settings.shifts = config.shifts || [];\n settings.shiftChangeCompensation = config.shiftChangeCompensation || 10;\n settings.lunchBreakMinutes = config.lunchBreakMinutes || 30;\n global.set(\"settings\", settings);\n\n node.status({ fill: \"green\", shape: \"dot\", text: `${config.shifts.length} sh ${config.shiftChangeCompensation} ch ${config.shiftChangeCompensation} lu` });\n return null;\n}\n\n// Save threshold config\nif (topic === \"saveThresholdConfig\") {\n const config = msg.payload || {};\n settings.thresholdMultiplier = config.thresholdMultiplier || 1.5;\n settings.macroStoppageMultiplier = config.macroStoppageMultiplier || 5;\n settings.oeeAlertThreshold = config.oeeAlertThreshold || 90;\n global.set(\"settings\", settings);\n\n node.status({ fill: \"green\", shape: \"dot\", text: `Threshold: ${config.thresholdMultiplier}x / ${config.macroStoppageMultiplier}x` });\n return null;\n}\n\n// Load shift config\nif (topic === \"getShiftConfig\") {\n msg.topic = \"shiftConfigData\";\n msg.payload = {\n shifts: settings.shifts || [{ start: \"08:00\", end: \"16:00\" }],\n shiftChangeCompensation: settings.shiftChangeCompensation || 10,\n lunchBreakMinutes: settings.lunchBreakMinutes || 30,\n thresholdMultiplier: settings.thresholdMultiplier || 1.5,\n macroStoppageMultiplier: settings.macroStoppageMultiplier || 5,\n oeeAlertThreshold: settings.oeeAlertThreshold || 90\n };\n return msg; // Send back to UI\n}\n\n// Save all settings at once\nif (topic === \"getShiftConfig\") {\n msg.topic = \"shiftConfigData\";\n msg.payload = {\n shifts: settings.shifts || [{ start: \"08:00\", end: \"16:00\" }],\n shiftChangeCompensation: settings.shiftChangeCompensation || 10,\n lunchBreakMinutes: settings.lunchBreakMinutes || 30,\n thresholdMultiplier: settings.thresholdMultiplier || 1.5,\n macroStoppageMultiplier: settings.macroStoppageMultiplier || 5,\n oeeAlertThreshold: settings.oeeAlertThreshold || 90\n };\n return msg; // Send back to UI\n}\n\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":720,"wires":[["b45e6bfabde1f79c"]]},{"id":"b45e6bfabde1f79c","type":"link out","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","name":"link out 17","mode":"link","links":["a51caffa7492c805"],"x":525,"y":720,"wires":[]},{"id":"2d893a89211ae18f","type":"inject","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"KPI Tick","props":[{"p":"payload"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":1200,"y":600,"wires":[["ddf6d786b5a7e682"]]},{"id":"6f5af40a0e8ea8be","type":"debug","z":"8ccf34b55a2afcad","name":"debug 2","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2360,"y":140,"wires":[]},{"id":"9a2690258e151b66","type":"function","z":"8ccf34b55a2afcad","name":"function 1","func":"let last = context.get('last') || 0;\nlet current = Number(msg.payload);\nif (current !== last) {\n context.set('last', current);\n return msg;\n}\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2400,"y":240,"wires":[["6f5af40a0e8ea8be","ac25819b6b50ab90"]]},{"id":"af0fb4f5a75407ac","type":"switch","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"","property":"topic","propertyType":"msg","rules":[{"t":"istype","v":"string","vt":"string"}],"checkall":"true","repair":false,"outputs":1,"x":1530,"y":820,"wires":[["037e4825e2537ed2"]]},{"id":"1a14505b6501d2dc","type":"change","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Reset saveKpis flag","rules":[{"t":"set","p":"state","pt":"global","to":"$merge([($globalContext(\"state\") ? $globalContext(\"state\") : {}), {\"saveKpis\": 0}])","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":2200,"y":660,"wires":[[]]},{"id":"e8efa837400e163b","type":"debug","z":"8ccf34b55a2afcad","name":"debug 4","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2140,"y":80,"wires":[]},{"id":"873eb9e13455403e","type":"rpi-gpio in","z":"8ccf34b55a2afcad","d":true,"name":"","pin":"17","intype":"up","debounce":"25","read":true,"bcm":true,"x":2220,"y":200,"wires":[["9a2690258e151b66"]]},{"id":"8c52b16f9c4671d0","type":"function","z":"8ccf34b55a2afcad","name":"Build Current State Snapshot","func":"// Build Current State Snapshot (outbox payload)\n\nconst config = global.get(\"config\") || {};\nconst machineId = config.machineId;\nconst now = Date.now();\nconst state = global.get(\"state\") || {};\nconst snapshot = state.lastState;\nconst settings = global.get(\"settings\") || {};\nconst lastMoldActive = Number(state.lastMoldActive ?? 0);\nconst moldActive = Number(\n snapshot?.activeWorkOrder?.cavities ??\n lastMoldActive ??\n snapshot?.cavities ??\n settings.moldActive ??\n global.get(\"moldActive\") ??\n 0\n);\nconst cavities = moldActive > 0 ? moldActive : null;\nmsg.tsMs = now;\n\n// This is the state you already have (from UI or from your state builder)\nconst s = msg.payload || {};\n\nmsg._mode = \"current-state\";\n\n// what you POST to cloud\nmsg.payload = {\n machineId: machineId,\n tsMs: now,\n activeWorkOrder: s.activeWorkOrder ?? null,\n cycle_count: s.cycleCount ?? null,\n good_parts: s.goodParts ?? null,\n scrap_parts: s.scrapParts ?? null,\n cavities,\n cycleTime: s.cycleTime ?? null,\n actualCycleTime: s.actualCycleTime ?? null,\n trackingEnabled: s.trackingEnabled ?? null,\n productionStarted: s.productionStarted ?? null,\n kpis: s.kpis ?? null,\n};\n\n// what goes into outbox (MUST MATCH what you intend to send)\nconst p = msg.payload || {};\n\nmsg.outbox = {\n type: \"kpi\",\n payload: {\n tsMs: p.tsMs,\n activeWorkOrder: p.activeWorkOrder,\n cycle_count: p.cycle_count,\n good_parts: p.good_parts,\n scrap_parts: p.scrap_parts,\n cavities: p.cavities,\n cycleTime: p.cycleTime,\n actualCycleTime: p.actualCycleTime,\n trackingEnabled: p.trackingEnabled,\n productionStarted: p.productionStarted,\n kpis: p.kpis,\n },\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2420,"y":980,"wires":[["7cd3a5e80eb2b6e1"]]},{"id":"a1e59ef54a91ee50","type":"function","z":"8ccf34b55a2afcad","name":"Build Event Outbox Payload","func":"// Build event outbox payload(s) — handle both single event and array (from Anomaly Detector).\n// Original single-event behavior preserved byte-for-byte for scrap-entry and\n// downtime-acknowledged paths. Array input emits one outbox message per anomaly.\n\nconst anomaly = global.get(\"anomaly\") || {};\n\nconst incoming = msg.payload;\nconst rawEvents = Array.isArray(incoming)\n ? incoming\n : (incoming && typeof incoming === \"object\" ? [incoming] : []);\nif (rawEvents.length === 0) return null;\n\nfor (const raw of rawEvents) {\n if (!raw || typeof raw !== \"object\") continue;\n\n const event = { ...raw };\n const tsMs = typeof event.tsMs === \"number\" ? event.tsMs : Date.now();\n\n const incidentKey =\n event.incidentKey ||\n event.incident_key ||\n (event.data && event.data.last_cycle_timestamp\n ? [event.anomaly_type || event.anomalyType || \"event\",\n event.work_order_id || event.workOrderId || \"\",\n String(event.data.last_cycle_timestamp)].join(\":\")\n : null);\n\n const reasonFromStore = incidentKey && anomaly.reasonsByIncident\n ? anomaly.reasonsByIncident[incidentKey]\n : null;\n if (!event.reason && reasonFromStore) event.reason = reasonFromStore;\n\n const anomalyType = event.anomaly_type || event.anomalyType || null;\n const isDowntimeType = anomalyType === \"microstop\" || anomalyType === \"macrostop\";\n if (!event.downtime) {\n event.downtime = isDowntimeType ? {\n incidentKey: incidentKey || null,\n anomalyType,\n durationSeconds: event.data && Number(event.data.stoppage_duration_seconds) || null\n } : null;\n }\n\n node.send({\n ...msg,\n tsMs,\n outbox: { type: \"event\", payload: { event } }\n });\n}\n\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2540,"y":780,"wires":[["4e12fb078508f92e"]]},{"id":"27df5e222b57f367","type":"inject","z":"8ccf34b55a2afcad","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":2290,"y":700,"wires":[["75380f6a82ade0da"]]},{"id":"75380f6a82ade0da","type":"function","z":"8ccf34b55a2afcad","name":"Online HeartBeat","func":"// Heartbeat Producer (feeds Outbox Enqueue v1)\n\nconst config = global.get(\"config\") || {};\nconst machineId = msg.machineId || config.machineId;\nif (!machineId) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Heartbeat waiting for pairing\" });\n return null;\n}\nmsg.machineId = machineId;\n\n// Edge heartbeat = \"I am alive and running Node-RED\"\nconst status = \"ONLINE\";\n\n// pull these from globals if possible (set once at boot)\nconst ip = config.edgeIp || msg.ip || \"192.168.18.33\";\nconst fwVersion = config.fwVersion || \"raspi-nodered-1.0\";\nconst message = \"NR heartbeat\";\n\n// ---- DEDUPE / THROTTLE ----\n// Only enqueue if changed OR interval elapsed\nconst now = Date.now();\nmsg.tsMs = now;\nconst intervalMs = Number(config.heartbeatIntervalMs || 15000);\n\n// Only include \"stable\" fields in signature; don't include timestamps\nconst signature = JSON.stringify({ status, ip, fwVersion });\n\nconst last = flow.get(\"hb_last\");\nif (last && last.signature === signature && (now - last.tsMs) < intervalMs) {\n return null; // skip enqueue (prevents spamming)\n}\nflow.set(\"hb_last\", { signature, tsMs: now });\n\n// This is what Outbox Enqueue v1 consumes\nmsg.outbox = {\n type: \"heartbeat\",\n payload: { status, message, ip, fwVersion },\n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2540,"y":640,"wires":[["0d19192a0f6a11dd"]]},{"id":"d685745ab029bddb","type":"function","z":"8ccf34b55a2afcad","name":"Build Cycle Outbox Payload","func":"// Build cycle outbox payload (direct HTTP disabled)\n\nconst config = global.get(\"config\") || {};\nconst machineId = config.machineId;\n\nif (!msg.cycleRow) return null;\n\nmsg.tsMs = typeof msg.cycleRow.tsMs === \"number\" ? msg.cycleRow.tsMs : Date.now();\n\n// Deduplicate (extra safety)\nconst dedupeKey = `cc:${msg.cycleRow.cycle_count}`;\nconst lastKey = flow.get(\"lastCyclePostedKey\");\nif (lastKey === dedupeKey) return null;\nflow.set(\"lastCyclePostedKey\", dedupeKey);\n\n// For debugging visibility\nmsg._debug = {\n machineId: machineId,\n cycle_count: msg.cycleRow.cycle_count,\n actual_cycle_time: msg.cycleRow.actual_cycle_time,\n theoretical_cycle_time: msg.cycleRow.theoretical_cycle_time,\n};\n\nmsg.outbox = {\n type: \"cycle\",\n payload: { cycle: msg.cycleRow },\n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2540,"y":720,"wires":[["4e12fb078508f92e"]]},{"id":"1f4b73857950a541","type":"http request","z":"8ccf34b55a2afcad","d":true,"name":"Legacy Direct Cycle HTTP (disabled)","method":"POST","ret":"txt","paytoqs":"ignore","url":"http://mis.maliountech.com.mx/api/ingest/cycle","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"Content-Type","keyValue":"","valueType":"other","valueValue":"application/json"},{"keyType":"other","keyValue":"x-api-key","valueType":"other","valueValue":"e0113ab0688769179fce30a44d21e4fd0576747b0886e9a1"}],"x":2770,"y":320,"wires":[["8a1b2c11abab88f9"]]},{"id":"8a1b2c11abab88f9","type":"debug","z":"8ccf34b55a2afcad","name":"debug 9","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2660,"y":180,"wires":[]},{"id":"70999189874246f1","type":"inject","z":"8ccf34b55a2afcad","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"0","payload":"1","payloadType":"num","x":2500,"y":360,"wires":[["9a2690258e151b66"]]},{"id":"4e12fb078508f92e","type":"subflow:c19582f27bf28841","z":"8ccf34b55a2afcad","name":"Outbox Enqueue v1","x":2920,"y":780,"wires":[["eb1f811daf227ecc","eafd06a1e9e1ff30"]]},{"id":"4ed85f42c5f02ddf","type":"inject","z":"8ccf34b55a2afcad","name":"Publisher tick","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.5,"topic":"","payload":"","payloadType":"date","x":2480,"y":920,"wires":[["eafd06a1e9e1ff30"]]},{"id":"94e5952704824302","type":"mysql","z":"8ccf34b55a2afcad","mydb":"fc9634aabefee16b","name":"Fetch pending outbox","x":3020,"y":880,"wires":[["4301be4fea74e008"]]},{"id":"eafd06a1e9e1ff30","type":"function","z":"8ccf34b55a2afcad","name":"Select pending batch","func":"// Assume data\n\n\n// Set the SQL query in msg.topic using named parameters\nmsg.topic = `SELECT id, machine_id, msg_type, endpoint, schema_version, seq, ts_device_ms,\n payload_json, attempts, next_attempt_at\nFROM outbox_messages\nWHERE status='pending'\n AND (next_attempt_at IS NULL OR next_attempt_at <= NOW())\nORDER BY id ASC\nLIMIT 25;\n`\n// Set the values in msg.payload as an object\n\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2800,"y":880,"wires":[["94e5952704824302","a591bb8f1f62f230"]]},{"id":"4301be4fea74e008","type":"switch","z":"8ccf34b55a2afcad","name":"Has rows?","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"","vt":"str"},{"t":"neq","v":"","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":3230,"y":880,"wires":[[],["8775ba662582c14f"]]},{"id":"8775ba662582c14f","type":"split","z":"8ccf34b55a2afcad","name":"Split rows","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":3420,"y":880,"wires":[["8962b5831f39dcf2"]]},{"id":"8962b5831f39dcf2","type":"function","z":"8ccf34b55a2afcad","name":"Build HTTP request","func":"const row = msg.payload; // row from DB\n\nconst config = global.get(\"config\") || {};\nconst base = config.cloudBaseUrl;\nconst apiKey = config.apiKey;\n\nconst machineId = config.machineId;\n\nif (machineId && row && row.machine_id && String(row.machine_id) !== String(machineId)) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Stale row (machine mismatch)\" });\n msg.topic = `\nUPDATE outbox_messages\nSET status='failed',\n last_http_status=?,\n last_error=?,\n next_attempt_at=NULL\nWHERE id=?;\n`.trim();\n msg.payload = [409, \"stale_machine\", Number(row.id)];\n return [null, msg];\n}\n\n\n\nif (!base || !apiKey) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Publisher waiting for pairing\" });\n return null;\n}\n\nconst baseUrl = String(base).replace(/\\/+$/, \"\");\nconst endpoint = String(row.endpoint || \"\");\nif (!endpoint.startsWith(\"/\")) throw new Error(\"Publisher: bad endpoint on row \" + row.id);\n\nmsg._row = row;\n\nmsg.method = row.method || \"POST\";\nmsg.url = baseUrl + endpoint;\n\nmsg.headers = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": apiKey,\n};\n\n// payload_json may be string (most common) or object depending on mysql node\nlet payload = row.payload_json;\nif (typeof payload === \"string\") {\n try { payload = JSON.parse(payload); } catch (e) { }\n}\nmsg.payload = payload;\n\nreturn msg;\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":3610,"y":880,"wires":[["6e92e862a70834f1","30d6d75548f0bc28"],["9166011d8123adef"]]},{"id":"3c747f2b199404f6","type":"http request","z":"8ccf34b55a2afcad","name":"Send outbox HTTP","method":"use","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":3710,"y":680,"wires":[["004d80a4f572b7dc"]]},{"id":"004d80a4f572b7dc","type":"switch","z":"8ccf34b55a2afcad","name":"HTTP 200?","property":"statusCode","propertyType":"msg","rules":[{"t":"eq","v":"200","vt":"str"},{"t":"neq","v":"200","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":3850,"y":880,"wires":[["9aa8958ed50e1a38"],["96e08462edb58122"]]},{"id":"9aa8958ed50e1a38","type":"function","z":"8ccf34b55a2afcad","name":"Mark Sent","func":"// Build Sent Update (use this right after HTTP request success branch)\nconst row = msg._row; // <-- IMPORTANT\nconst status = Number(msg.statusCode ?? 0);\n\nmsg.topic = `\n UPDATE outbox_messages\n SET status='sent',\n sent_at=NOW(),\n last_http_status=?,\n last_error=NULL\n WHERE id=?;\n`.trim();\n\nmsg.payload = [status, Number(row.id)];\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":4020,"y":860,"wires":[["9166011d8123adef"]]},{"id":"96e08462edb58122","type":"function","z":"8ccf34b55a2afcad","name":"Retry","func":"const row = msg._row;\nconst attempts = Number(row.attempts || 0) + 1;\n\nconst retryConfig = {\n shortDelaySec: 5,\n mediumDelaySec: 30,\n longDelaySec: 180,\n mediumAfter: 5,\n longAfter: 20,\n errorMaxLen: 450,\n};\n\nconst status = Number(msg.statusCode || 0);\nlet err = \"\";\nif (msg.payload && typeof msg.payload === \"object\") {\n err = JSON.stringify(msg.payload).slice(0, retryConfig.errorMaxLen);\n} else if (msg.payload != null) {\n err = String(msg.payload).slice(0, retryConfig.errorMaxLen);\n} else {\n err = \"request_failed\";\n}\n\nif (status === 401 || status === 403) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Unauthorized - re-pair\" });\n msg.topic = `\nUPDATE outbox_messages\nSET status='failed',\n last_http_status=?,\n last_error=?,\n next_attempt_at=NULL\nWHERE id=?;\n`.trim();\n msg.payload = [ status || null, err, Number(row.id) ];\n return msg;\n}\n\n// Backoff policy\nlet delaySec = retryConfig.shortDelaySec;\nif (attempts <= retryConfig.mediumAfter) delaySec = retryConfig.shortDelaySec;\nelse if (attempts <= retryConfig.longAfter) delaySec = retryConfig.mediumDelaySec;\nelse delaySec = retryConfig.longDelaySec;\n\nmsg.topic = `\nUPDATE outbox_messages\nSET attempts=?,\n next_attempt_at=DATE_ADD(NOW(), INTERVAL ? SECOND),\n last_http_status=?,\n last_error=?\nWHERE id=?;\n`.trim();\n\nmsg.payload = [ attempts, delaySec, status || null, err, Number(row.id) ];\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":4010,"y":900,"wires":[["9166011d8123adef"]]},{"id":"9166011d8123adef","type":"mysql","z":"8ccf34b55a2afcad","mydb":"fc9634aabefee16b","name":"Update outbox status","x":4450,"y":900,"wires":[["d1d1cbe0ccc7236b"]]},{"id":"c48444f30fc71bae","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"State Accumulator global","func":"const config = global.get(\"config\") || {};\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\nconst machineId = config.machineId; // set this once at boot (see below)\nif (!machineId) { node.warn(\"lastState: missing config.machineId\"); return null; }\nconst active = state.activeWorkOrder || null;\nconst trackingEnabled = !!state.trackingEnabled;\nconst productionStarted = !!state.productionStarted;\nconst kpis = msg.kpis || state.currentKPIs || { oee: 0, availability: 0, performance: 0, quality: 0 };\nconst moldByWorkOrder = state.moldByWorkOrder || {};\nconst moldActive = Number(\n active?.cavities ??\n moldByWorkOrder[active?.id]?.active ??\n state.lastMoldActive ??\n settings.moldActive ??\n global.get(\"moldActive\") ??\n 0\n);\nconst cavities = moldActive > 0 ? moldActive : null;\n\nconst lastState = {\n machineId,\n activeWorkOrder: active,\n cycleCount: Number(state.cycleCount ?? 0),\n goodParts: Number(active?.goodParts ?? 0),\n scrapParts: Number(active?.scrapParts ?? 0),\n cavities,\n cycleTime: Number(active?.cycleTime ?? state.lastCycleTime ?? 0),\n actualCycleTime: Number(state.lastActualCycleTime ?? 0),\n trackingEnabled,\n productionStarted,\n kpis,\n tsMs: Date.now(),\n};\n\nstate.lastState = lastState;\nglobal.set(\"state\", state);\nmsg.lastState = lastState;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1690,"y":760,"wires":[[]]},{"id":"785db0c7cb865cc2","type":"inject","z":"8ccf34b55a2afcad","name":"KPI state getter","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":2210,"y":1020,"wires":[["0ce8e4e0ba61ec80"]]},{"id":"0ce8e4e0ba61ec80","type":"function","z":"8ccf34b55a2afcad","name":"Build KPI Outbox from lastState","func":"// Build KPI Outbox from lastState\n\nconst config = global.get(\"config\") || {};\nconst state = global.get(\"state\") || {};\nconst snapshot = state.lastState;\nconst settings = global.get(\"settings\") || {};\nconst moldByWorkOrder = state.moldByWorkOrder || {};\nconst moldActive = Number(settings.moldActive ?? global.get(\"moldActive\") ?? snapshot?.activeWorkOrder?.cavities ?? 0);\n//const cavities = moldActive > 0 ? moldActive : snapshot?.cavities ?? null;\n\nif (!snapshot) return null;\n\nconst machineId = snapshot.machineId || config.machineId;\nif (!machineId) return null;\n\nmsg.machineId = machineId;\n\nconst activeWorkOrder = snapshot?.activeWorkOrder ? { ...snapshot.activeWorkOrder } : null;\nif (activeWorkOrder?.lastUpdateIso && typeof activeWorkOrder.lastUpdateIso !== \"string\") {\n delete activeWorkOrder.lastUpdateIso;\n}\n\nconst cavitiesRaw = Number(\n activeWorkOrder?.cavities ??\n moldByWorkOrder[activeWorkOrder?.id]?.active ??\n snapshot?.cavities ??\n state.lastMoldActive ??\n settings.moldActive ??\n global.get(\"moldActive\") ??\n 0\n);\nconst cavities = cavitiesRaw > 0 ? cavitiesRaw : null;\n\n\n\nmsg.outbox = {\n type: \"kpi\",\n payload: {\n tsMs: snapshot.tsMs,\n activeWorkOrder,\n cycle_count: snapshot.cycleCount,\n good_parts: snapshot.goodParts,\n scrap_parts: snapshot.scrapParts,\n cavities,\n cycleTime: snapshot.cycleTime,\n actualCycleTime: snapshot.actualCycleTime,\n trackingEnabled: snapshot.trackingEnabled,\n productionStarted: snapshot.productionStarted,\n kpis: snapshot.kpis,\n },\n};\n\nmsg.tsMs = typeof snapshot.tsMs === \"number\" ? snapshot.tsMs : Date.now();\n\n// keep timestamp so you can see it ticking\nmsg.payload = msg.tsMs;\nmsg.topic = \"\";\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2450,"y":1020,"wires":[["818de86c22724aa5"]]},{"id":"eb1f811daf227ecc","type":"debug","z":"8ccf34b55a2afcad","name":"debug 1","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":3190,"y":1120,"wires":[]},{"id":"d1d1cbe0ccc7236b","type":"debug","z":"8ccf34b55a2afcad","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":4540,"y":1100,"wires":[]},{"id":"7cd3a5e80eb2b6e1","type":"debug","z":"8ccf34b55a2afcad","name":"debug 5","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2590,"y":1220,"wires":[]},{"id":"818de86c22724aa5","type":"debug","z":"8ccf34b55a2afcad","name":"debug 6","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":3030,"y":1200,"wires":[]},{"id":"6e92e862a70834f1","type":"debug","z":"8ccf34b55a2afcad","name":"debug 7","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":3400,"y":1080,"wires":[]},{"id":"a591bb8f1f62f230","type":"debug","z":"8ccf34b55a2afcad","name":"debug 8","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":3050,"y":1020,"wires":[]},{"id":"917065027876ecd6","type":"debug","z":"8ccf34b55a2afcad","name":"debug 10","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2010,"y":1080,"wires":[]},{"id":"5c86dbb1f8044c9b","type":"debug","z":"8ccf34b55a2afcad","name":"debug 11","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2000,"y":1140,"wires":[]},{"id":"03fa4c64efd95c8a","type":"inject","z":"8ccf34b55a2afcad","name":"Init config inject","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":3250,"y":220,"wires":[["a9fa224f48b83f71"]]},{"id":"c904233ea449e9e0","type":"function","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","name":"Pair Machine Request","func":"const topic = msg.topic || \"\";\nif (topic != \"pairMachine\") {\n return null;\n}\n\nconst raw = (msg.payload && (msg.payload.code || msg.payload.pairingCode || msg.payload)) || \"\";\nconst code = String(raw).trim().toUpperCase().replace(/[^A-Z0-9]/g, \"\");\n\nif (code.length != 5) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Bad code\" });\n return [null, { topic: \"pairMachineResult\", payload: { ok: false, error: \"Codigo invalido.\" } }];\n}\n\nconst config = global.get(\"config\") || {};\nconst baseUrl = String(config.cloudBaseUrl || \"https://mis.maliountech.com.mx\").replace(/\\/+$/, \"\");\n\nmsg.method = \"POST\";\nmsg.url = baseUrl + \"/api/machines/pair\";\nmsg.headers = { \"Content-Type\": \"application/json\" };\nmsg.payload = { code: code };\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: \"Pairing...\" });\nreturn [msg, null];\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":780,"wires":[["053e4c241fa29315"],["b45e6bfabde1f79c"]]},{"id":"053e4c241fa29315","type":"http request","z":"8ccf34b55a2afcad","name":"Pair Machine HTTP","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":640,"y":780,"wires":[["60056bf2030382bc"]]},{"id":"60056bf2030382bc","type":"function","z":"8ccf34b55a2afcad","g":"c04e913f614d9036","name":"Pair Machine Response","func":"let res = msg.payload;\nif (typeof res == \"string\") {\n try { res = JSON.parse(res); } catch (e) { res = { error: res }; }\n}\nres = res || {};\n\nconst ok = res.ok === true || res.success === true;\nconst cfg = res.config || res;\n\nif (ok && cfg && cfg.machineId && cfg.apiKey) {\n const current = global.get(\"config\") || {};\n current.cloudBaseUrl = cfg.cloudBaseUrl || current.cloudBaseUrl;\n current.machineId = cfg.machineId;\n current.apiKey = cfg.apiKey;\n current.orgId = cfg.orgId || current.orgId;\n global.set(\"config\", current);\n\n node.status({ fill: \"green\", shape: \"dot\", text: \"Paired\" });\n msg.topic = \"pairMachineResult\";\n msg.payload = { ok: true, machineId: current.machineId };\n return msg;\n}\n\nconst error = (res && (res.error || res.message)) || (msg.statusCode ? (\"HTTP \" + msg.statusCode) : \"Pairing failed\");\nnode.status({ fill: \"red\", shape: \"ring\", text: \"Pair failed\" });\nmsg.topic = \"pairMachineResult\";\nmsg.payload = { ok: false, error: error };\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":860,"y":780,"wires":[["b45e6bfabde1f79c","34fe41a1d67fb9cd"]]},{"id":"5eeeac992826fa3b","type":"link in","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"link into settings","links":[],"x":255,"y":440,"wires":[["372edd599e1b2fdd"]]},{"id":"3dd6baf608946fde","type":"switch","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"Wifi switch","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"wifi:scan","vt":"str"},{"t":"eq","v":"wifi:status","vt":"str"},{"t":"eq","v":"wifi:apply","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":700,"y":460,"wires":[["799fcf989e49f86a"],["5a1b8c2451faa879"],["d7603f458aa33b0d"]]},{"id":"5a1b8c2451faa879","type":"link out","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"wifi:status from settings","mode":"link","links":[],"x":815,"y":460,"wires":[]},{"id":"799fcf989e49f86a","type":"link out","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"wifi:scan from settings","mode":"link","links":[],"x":815,"y":420,"wires":[]},{"id":"d7603f458aa33b0d","type":"link out","z":"8ccf34b55a2afcad","g":"472f828e204736a6","name":"wifi:apply from settings","mode":"link","links":[],"x":815,"y":500,"wires":[]},{"id":"c238344865721014","type":"function","z":"8ccf34b55a2afcad","name":"Parse settings update","func":"let payload = msg.payload;\n\nif (Buffer.isBuffer(payload)) {\n payload = payload.toString(\"utf8\");\n}\n\nif (typeof payload === \"string\") {\n try {\n payload = JSON.parse(payload);\n } catch (err) {\n payload = {};\n }\n}\n\nif (!payload || typeof payload !== \"object\") {\n payload = {};\n}\n\nconst topic = String(msg.topic || \"\");\nconst parts = topic.split(\"/\");\n\nfunction pickPart(label) {\n const idx = parts.indexOf(label);\n if (idx === -1) return null;\n return parts[idx + 1] || null;\n}\n\nconst orgId = payload.orgId || pickPart(\"org\");\nconst machineId = payload.machineId || pickPart(\"machines\");\n\nif (orgId) payload.orgId = orgId;\nif (machineId) payload.machineId = machineId;\n\nmsg.payload = payload;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":920,"wires":[["ce21bcd6fddd75ef"]]},{"id":"ce21bcd6fddd75ef","type":"function","z":"8ccf34b55a2afcad","name":"Fetch settings from Control Tower","func":"const config = global.get(\"config\") || {};\nconst base = config.cloudBaseUrl;\nconst apiKey = config.apiKey;\nconst machineId = config.machineId;\n\nif (!base || !apiKey || !machineId) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Settings fetch waiting for pairing\" });\n return null;\n}\n\nconst update = msg.payload || {};\nconst forced = msg.topic === \"settingsRefresh\" || msg.forceRefresh === true;\n\nif (!forced) {\n if (update.machineId && update.machineId !== machineId) {\n return null;\n }\n if (config.orgId && update.orgId && update.orgId !== config.orgId) {\n return null;\n }\n}\n\nconst baseUrl = String(base).replace(/\\/+$/, \"\");\nmsg.method = \"GET\";\nmsg.url = baseUrl + \"/api/settings/machines/\" + machineId;\nmsg.headers = {\n \"x-api-key\": apiKey,\n \"Accept\": \"application/json\"\n};\n\nmsg._settingsUpdate = {\n orgId: update.orgId || config.orgId,\n machineId: update.machineId || machineId,\n version: update.version\n};\nmsg._settingsRequestedAt = Date.now();\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":920,"wires":[["59c3cac2af12053e"]]},{"id":"59c3cac2af12053e","type":"http request","z":"8ccf34b55a2afcad","name":"Fetch settings HTTP","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":1020,"y":920,"wires":[["cc02262fda7af805"]]},{"id":"cc02262fda7af805","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Apply settings + update UI","func":"let payload = msg.payload;\n\nif (Buffer.isBuffer(payload)) {\n payload = payload.toString(\"utf8\");\n}\n\nif (typeof payload === \"string\") {\n try {\n payload = JSON.parse(payload);\n } catch (err) {\n payload = {};\n }\n}\n\nif (!payload || payload.ok === false) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Settings fetch failed\" });\n return [null, null];\n}\n\nconst effective = payload.effectiveSettings || payload.settings || payload;\nif (!effective || typeof effective !== \"object\") {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Settings payload missing\" });\n return [null, null];\n}\n\nconst defaults = effective.defaults || {};\nconst shiftSchedule = effective.shiftSchedule || {};\nconst thresholds = effective.thresholds || {};\nconst incomingCatalog = effective.reasonCatalog || null;\n\nconst settings = global.get(\"settings\") || {};\n\nconst nextMoldTotal = Number(defaults.moldTotal ?? settings.moldTotal ?? 0);\nconst nextMoldActive = Number(defaults.moldActive ?? settings.moldActive ?? 0);\n\nsettings.moldTotal = nextMoldTotal;\nsettings.moldActive = nextMoldActive;\n\nif (Array.isArray(shiftSchedule.shifts) && shiftSchedule.shifts.length) {\n settings.shifts = shiftSchedule.shifts.map((s) => ({\n start: s.start,\n end: s.end\n }));\n} else if (!Array.isArray(settings.shifts)) {\n settings.shifts = [{ start: \"08:00\", end: \"16:00\" }];\n}\n\nsettings.shiftChangeCompensation = Number(\n shiftSchedule.shiftChangeCompensationMin ?? settings.shiftChangeCompensation ?? 10\n);\nsettings.lunchBreakMinutes = Number(\n shiftSchedule.lunchBreakMin ?? settings.lunchBreakMinutes ?? 30\n);\n\nsettings.thresholdMultiplier = Number(\n thresholds.stoppageMultiplier ?? settings.thresholdMultiplier ?? 1.5\n);\nsettings.macroStoppageMultiplier = Number(\n thresholds.macroStoppageMultiplier ?? settings.macroStoppageMultiplier ?? 5\n);\nsettings.oeeAlertThreshold = Number(\n thresholds.oeeAlertThresholdPct ?? settings.oeeAlertThreshold ?? 90\n);\n\nconst normalizeCatalogItems = (list, fallbackLabelPrefix) => {\n if (!Array.isArray(list)) return [];\n return list\n .map((c, idx) => {\n const categoryId = String(c.id || c.categoryId || (\"cat_\" + idx));\n const categoryLabel = String(c.label || c.categoryLabel || (fallbackLabelPrefix + \" \" + (idx + 1)));\n const detailsRaw = Array.isArray(c.children) ? c.children : (Array.isArray(c.details) ? c.details : []);\n const details = detailsRaw.map((d, jdx) => ({\n id: String(d.id || d.detailId || (categoryId + \"_d\" + jdx)),\n label: String(d.label || d.detailLabel || (\"Detalle \" + (jdx + 1)))\n }));\n return {\n id: categoryId,\n label: categoryLabel,\n children: details\n };\n })\n .filter((c) => c.label && c.children.length > 0);\n};\n\nconst currentCatalog = settings.reasonCatalog || {};\nconst nextCatalogVersion =\n Number(\n (incomingCatalog && incomingCatalog.version) ??\n effective.reasonCatalogVersion ??\n currentCatalog.version ??\n 1\n ) || 1;\n\nconst hasIncomingCatalog = !!(incomingCatalog && (Array.isArray(incomingCatalog.downtime) || Array.isArray(incomingCatalog.scrap)));\nconst normalizedIncoming = hasIncomingCatalog ? {\n version: nextCatalogVersion,\n downtime: normalizeCatalogItems(incomingCatalog.downtime || [], \"Paro\"),\n scrap: normalizeCatalogItems(incomingCatalog.scrap || [], \"Scrap\")\n} : null;\n\nconst fallbackCatalog = {\n version: Number(currentCatalog.version || nextCatalogVersion || 1),\n downtime: normalizeCatalogItems(currentCatalog.downtime || [], \"Paro\"),\n scrap: normalizeCatalogItems(currentCatalog.scrap || [], \"Scrap\")\n};\n\nsettings.reasonCatalog = normalizedIncoming || fallbackCatalog;\nsettings.reasonCatalog.version = Number(settings.reasonCatalog.version || 1);\n\n\nif (effective.version !== undefined) {\n settings.version = Number(effective.version);\n}\n\nglobal.set(\"settings\", settings);\ntry {\n global.set(\"settings\", settings, \"file\");\n} catch (err) {\n // ignore if file store is not configured\n}\nglobal.set(\"moldActive\", settings.moldActive);\nglobal.set(\"moldTotal\", settings.moldTotal);\n\nconst config = global.get(\"config\") || {};\nif (effective.orgId && !config.orgId) {\n config.orgId = effective.orgId;\n global.set(\"config\", config);\n}\n\nnode.status({ fill: \"green\", shape: \"dot\", text: \"Settings synced\" });\n\nconst uiConfigMsg = {\n topic: \"shiftConfigData\",\n payload: {\n shifts: settings.shifts || [],\n shiftChangeCompensation: settings.shiftChangeCompensation || 10,\n lunchBreakMinutes: settings.lunchBreakMinutes || 30,\n thresholdMultiplier: settings.thresholdMultiplier || 1.5,\n macroStoppageMultiplier: settings.macroStoppageMultiplier || 5,\n oeeAlertThreshold: settings.oeeAlertThreshold || 90\n }\n};\n\n\nconst uiMoldMsg = {\n topic: \"moldPresetSelected\",\n payload: {\n total: settings.moldTotal || 0,\n active: settings.moldActive || 0\n }\n};\n\nconst readOnly = config.settingsReadOnly !== false;\nconst uiReadOnlyMsg = { topic: \"settingsReadOnly\", payload: readOnly };\nconst uiReasonCatalogMsg = {\n topic: \"reasonCatalogData\",\n payload: settings.reasonCatalog\n};\n\nnode.send([uiConfigMsg, null]);\nnode.send([uiMoldMsg, null]);\nnode.send([uiReadOnlyMsg, null]);\nnode.send([uiReasonCatalogMsg, null]);\n\nconst update = msg._settingsUpdate || {};\nconst orgId = update.orgId || config.orgId || effective.orgId;\nconst machineId = update.machineId || config.machineId;\nconst version = Number(effective.version ?? update.version ?? 0);\n\nif (!orgId || !machineId) {\n return [null, null];\n}\n\nconst prefix = String(config.mqttTopicPrefix || \"mis\").replace(/\\/+$/, \"\");\nconst ackTopic = prefix + \"/org/\" + orgId + \"/machines/\" + machineId + \"/settings/ack\";\n\nconst ackMsg = {\n topic: ackTopic,\n payload: JSON.stringify({\n type: \"settings_ack\",\n orgId,\n machineId,\n version,\n source: \"node-red\",\n ts: new Date().toISOString()\n })\n};\n\nreturn [null, ackMsg];\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1280,"y":920,"wires":[["b45e6bfabde1f79c","87dc48bbe753ab8d","25e40f96b0876550"],[]]},{"id":"d063639e9402cb47","type":"inject","z":"8ccf34b55a2afcad","name":"Refresh settings (poll)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"600","crontab":"","once":true,"onceDelay":1,"topic":"settingsRefresh","payload":"","payloadType":"date","x":240,"y":980,"wires":[["ce21bcd6fddd75ef"]]},{"id":"8abddc40064f1f64","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Fetch work orders from Control Tower","func":"const config = global.get(\"config\") || {};\nconst base = config.cloudBaseUrl;\nconst apiKey = config.apiKey;\nconst machineId = config.machineId;\n\nif (!base || !apiKey || !machineId) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Work orders fetch waiting for pairing\" });\n return null;\n}\n\nconst update = msg.payload || {};\nif (update.machineId && update.machineId !== machineId) {\n return null;\n}\nif (config.orgId && update.orgId && update.orgId !== config.orgId) {\n return null;\n}\n\nconst baseUrl = String(base).replace(/\\/+$/, \"\");\nmsg.method = \"GET\";\nmsg.url = baseUrl + \"/api/work-orders/machines/\" + machineId;\nmsg.headers = {\n \"x-api-key\": apiKey,\n \"Accept\": \"application/json\"\n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1770,"y":320,"wires":[["16f0e85a7bc88164"]]},{"id":"16f0e85a7bc88164","type":"http request","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Fetch work orders HTTP","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":2060,"y":320,"wires":[["65c3c2889a068c25","6248f46d1587526f"]]},{"id":"65c3c2889a068c25","type":"function","z":"8ccf34b55a2afcad","g":"1d1ce0cb54c52345","name":"Upsert work orders to local DB","func":"let payload = msg.payload;\n\nif (Buffer.isBuffer(payload)) {\n payload = payload.toString(\"utf8\");\n}\n\nif (typeof payload === \"string\") {\n try {\n payload = JSON.parse(payload);\n } catch (err) {\n payload = {};\n }\n}\n\nif (!payload || payload.ok === false) {\n node.status({\n fill: \"red\",\n shape: \"ring\",\n text: \"Work orders fetch failed\",\n });\n return null;\n}\n\nconst list = Array.isArray(payload.workOrders)\n ? payload.workOrders\n : Array.isArray(payload.orders)\n ? payload.orders\n : [];\n\nif (!list.length) {\n node.status({\n fill: \"yellow\",\n shape: \"ring\",\n text: \"No work orders\",\n });\n return null;\n}\n\nconst seen = new Set();\nconst values = [];\n\nlist.forEach((order) => {\n const id = String(\n order.workOrderId ||\n order.id ||\n order.work_order_id ||\n \"\"\n ).trim();\n\n if (!id || seen.has(id)) return;\n seen.add(id);\n\n const sku = String(order.sku || \"\").trim();\n\n const targetQtyRaw =\n order.targetQty ??\n order.target_qty ??\n order.target ??\n 0;\n\n const cycleTimeRaw =\n order.cycleTime ??\n order.theoreticalCycleTime ??\n order.theoretical_cycle_time ??\n 0;\n\n const targetQty = Number.isFinite(Number(targetQtyRaw))\n ? Math.trunc(Number(targetQtyRaw))\n : 0;\n\n const cycleTime = Number.isFinite(Number(cycleTimeRaw))\n ? Number(cycleTimeRaw)\n : 0;\n\n values.push([id, sku, targetQty, cycleTime, \"PENDING\"]);\n});\n\nif (!values.length) {\n node.status({\n fill: \"yellow\",\n shape: \"ring\",\n text: \"No valid work orders\",\n });\n return null;\n}\n\nmsg.topic = `\n INSERT INTO work_orders\n (work_order_id, sku, target_qty, cycle_time, status)\n VALUES ?\n ON DUPLICATE KEY UPDATE\n work_order_id = work_order_id;\n`;\n\nmsg.payload = [values];\nmsg._mode = \"sync-work-orders\";\n\nnode.status({\n fill: \"green\",\n shape: \"dot\",\n text: `Synced ${values.length} work orders`,\n});\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2310,"y":320,"wires":[["037e4825e2537ed2","13972bfb31607398"]]},{"id":"f983898f9906d911","type":"debug","z":"8ccf34b55a2afcad","name":"debug 15","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1670,"y":1340,"wires":[]},{"id":"6248f46d1587526f","type":"debug","z":"8ccf34b55a2afcad","name":"debug 16","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1860,"y":1300,"wires":[]},{"id":"13972bfb31607398","type":"debug","z":"8ccf34b55a2afcad","name":"debug 17","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2020,"y":1300,"wires":[]},{"id":"1d01fcfb16429d32","type":"inject","z":"8ccf34b55a2afcad","name":"Refresh work orders (poll)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"86400","crontab":"","once":true,"onceDelay":1,"topic":"workOrdersRefresh","payload":"","payloadType":"date","x":1350,"y":1240,"wires":[["8abddc40064f1f64"]]},{"id":"17b04a44024da6f2","type":"debug","z":"8ccf34b55a2afcad","name":"debug 18","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1690,"y":1120,"wires":[]},{"id":"254622f455de3a4c","type":"debug","z":"8ccf34b55a2afcad","name":"debug 19","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2270,"y":1220,"wires":[]},{"id":"7bd006e12583f8e1","type":"debug","z":"8ccf34b55a2afcad","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":960,"y":220,"wires":[]},{"id":"fa4a2d371b22788a","type":"debug","z":"8ccf34b55a2afcad","name":"debug 21","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1710,"y":1060,"wires":[]},{"id":"4ea28603310c2aef","type":"debug","z":"8ccf34b55a2afcad","name":"debug 22","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1770,"y":1000,"wires":[]},{"id":"30d6d75548f0bc28","type":"function","z":"8ccf34b55a2afcad","name":"function 3","func":"const awo = msg.payload?.activeWorkOrder;\nif (awo) {\n const v = awo.lastUpdateIso;\n\n // If it's already a string, keep it\n if (typeof v === \"string\") {\n // ok\n } else if (v instanceof Date) {\n awo.lastUpdateIso = v.toISOString();\n } else if (v && typeof v.toISO === \"function\") {\n // Luxon DateTime\n awo.lastUpdateIso = v.toISO();\n } else if (v && typeof v.toISOString === \"function\") {\n // Some date-like objects\n awo.lastUpdateIso = v.toISOString();\n } else if (v && typeof v.format === \"function\") {\n // Moment-like\n awo.lastUpdateIso = v.format();\n } else {\n // Last resort: try to stringify, otherwise null it out\n awo.lastUpdateIso = v ? String(v) : null;\n }\n node.warn(`[lastUpdateIso] typeof=${typeof v} value=${v}`);\n node.warn(`[lastUpdateIso] JSON=${JSON.stringify(v)}`);\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":3560,"y":740,"wires":[["3c747f2b199404f6"]]},{"id":"0d19192a0f6a11dd","type":"function","z":"8ccf34b55a2afcad","name":"Build Heartbeat HTTP","func":"// Build direct heartbeat HTTP request\nconst config = global.get(\"config\") || {};\nconst base = config.cloudBaseUrl;\nconst apiKey = config.apiKey;\n\nif (!base || !apiKey) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Heartbeat waiting for pairing\" });\n return null;\n}\n\nconst baseUrl = String(base).replace(/\\/+$/, \"\");\nmsg.method = \"POST\";\nmsg.url = baseUrl + \"/api/ingest/heartbeat\";\nmsg.headers = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": apiKey,\n};\n\n// Use the same payload you already build in Online HeartBeat\nmsg.payload = {\n machineId: msg.machineId,\n tsMs: msg.tsMs, // device time; server will ignore for last_seen\n status: \"ONLINE\",\n message: \"NR heartbeat\",\n ip: (config.edgeIp || msg.ip || \"192.168.18.33\"),\n fwVersion: (config.fwVersion || \"raspi-nodered-1.0\"),\n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2800,"y":640,"wires":[["580d486714edfc8c"]]},{"id":"580d486714edfc8c","type":"http request","z":"8ccf34b55a2afcad","name":"Send Heartbeat","method":"use","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":3040,"y":640,"wires":[["17aa949bd6c8b667"]]},{"id":"17aa949bd6c8b667","type":"debug","z":"8ccf34b55a2afcad","name":"debug 24","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":3210,"y":660,"wires":[]},{"id":"c3ef5e9b98a8e24f","type":"debug","z":"8ccf34b55a2afcad","name":"debug 25","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":970,"y":380,"wires":[]},{"id":"5c8843053781fbd9","type":"debug","z":"8ccf34b55a2afcad","name":"debug 26","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":960,"y":420,"wires":[]},{"id":"fddb2e595128c225","type":"inject","z":"8ccf34b55a2afcad","g":"c95c05b78d8464c5","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":1260,"y":240,"wires":[["6790210592277c61"]]},{"id":"6790210592277c61","type":"function","z":"8ccf34b55a2afcad","d":true,"g":"c95c05b78d8464c5","name":"Simula Inyectora","func":"/**\n * Machine Cycle Simulator (0/1 square wave with realistic variance)\n *\n * How to use:\n * - Trigger this node ONCE (Inject once after deploy). It will start emitting 0/1 on its own.\n * - To stop: send a message with msg.payload = \"stop\" (or msg.topic=\"stop\")\n * - To reset state: msg.payload = \"reset\"\n *\n * Output:\n * - msg.payload = 0 or 1\n */\n\nconst CFG = {\n halfPeriodMs: 7250,\n jitterMs: 250,\n perfectRunMs: 30 * 60 * 1000,\n pEnterPerfect: 0, // was 0.12 — disable perfect runs\n pSlowPhase: 0, // was 0.10 — disable slow phases\n pMicroStop: 0.5, // was 0.06 — force frequent microstops\n pMacroStop: 0.2, // was 0.008 — force frequent macrostops\n microStopMs: [5000, 15000],\n macroStopMs: [60 * 1000, 120 * 1000], // shorter macros (1-2 min) so you don't wait 8 min\n slowFactor: [1.25, 1.9],\n slowPhaseToggles: [6, 25],\n minGapMicroMs: 10 * 1000, // was 20s — allow faster retriggering\n minGapMacroMs: 30 * 1000, // was 60s\n};\n\nfunction randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }\nfunction randFloat(min, max) { return Math.random() * (max - min) + min; }\nfunction pickMs(range) { return randInt(range[0], range[1]); }\nfunction clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }\n\nfunction clearTimer() {\n const t = context.get(\"timer\");\n if (t) clearTimeout(t);\n context.set(\"timer\", null);\n context.set(\"running\", false);\n}\n\nif (msg && (msg.payload === \"stop\" || msg.topic === \"stop\")) {\n clearTimer();\n node.status({ fill: \"grey\", shape: \"ring\", text: \"stopped\" });\n return null;\n}\n\nif (msg && msg.payload === \"reset\") {\n context.set(\"state\", 0);\n node.status({ fill: \"grey\", shape: \"dot\", text: \"reset to 0\" });\n return null;\n}\n\n// Store a template msg (so you can pass machineId/topic/etc. once)\nif (msg && typeof msg === \"object\") {\n const template = Object.assign({}, msg);\n delete template.payload; // we'll overwrite payload each emit\n context.set(\"template\", template);\n}\n\nlet running = context.get(\"running\") || false;\nlet timer = context.get(\"timer\");\nlet state = context.get(\"state\");\nif (state === undefined || state === null) state = 0;\n\nlet phase = context.get(\"phase\") || { type: \"idle\", until: 0, slowLeft: 0 };\nlet lastStopAt = context.get(\"lastStopAt\") || 0;\n\nfunction decideNextDelayMs() {\n const now = Date.now();\n\n // Perfect run active\n if (phase.type === \"perfect\" && now < phase.until) {\n const jitter = randInt(-CFG.jitterMs, CFG.jitterMs);\n return clamp(CFG.halfPeriodMs + jitter, 300, 30 * 60 * 1000);\n }\n\n // Slow phase active (counted in toggles)\n if (phase.type === \"slow\" && phase.slowLeft > 0) {\n phase.slowLeft -= 1;\n context.set(\"phase\", phase);\n\n const factor = randFloat(CFG.slowFactor[0], CFG.slowFactor[1]);\n const jitter = randInt(-CFG.jitterMs, CFG.jitterMs);\n node.status({ fill: \"blue\", shape: \"dot\", text: `slow (${phase.slowLeft} left)` });\n return clamp(Math.round(CFG.halfPeriodMs * factor) + jitter, 300, 30 * 60 * 1000);\n }\n\n // Phase ended → maybe enter a new perfect run\n if (Math.random() < CFG.pEnterPerfect) {\n phase = { type: \"perfect\", until: now + CFG.perfectRunMs, slowLeft: 0 };\n context.set(\"phase\", phase);\n node.status({ fill: \"green\", shape: \"dot\", text: \"perfect run\" });\n return decideNextDelayMs();\n }\n\n // Event selection (macro > micro > slow > normal)\n const sinceStop = now - lastStopAt;\n\n if (sinceStop > CFG.minGapMacroMs && Math.random() < CFG.pMacroStop) {\n lastStopAt = now;\n context.set(\"lastStopAt\", lastStopAt);\n node.status({ fill: \"red\", shape: \"dot\", text: \"MACRO STOP\" });\n return pickMs(CFG.macroStopMs);\n }\n\n if (sinceStop > CFG.minGapMicroMs && Math.random() < CFG.pMicroStop) {\n lastStopAt = now;\n context.set(\"lastStopAt\", lastStopAt);\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"micro stop\" });\n return pickMs(CFG.microStopMs);\n }\n\n if (Math.random() < CFG.pSlowPhase) {\n phase = { type: \"slow\", until: 0, slowLeft: randInt(CFG.slowPhaseToggles[0], CFG.slowPhaseToggles[1]) };\n context.set(\"phase\", phase);\n return decideNextDelayMs();\n }\n\n // Normal running\n node.status({ fill: \"green\", shape: \"dot\", text: \"running\" });\n const jitter = randInt(-CFG.jitterMs, CFG.jitterMs);\n return clamp(CFG.halfPeriodMs + jitter, 300, 30 * 60 * 1000);\n}\n\nfunction emitTick() {\n // Toggle 0/1\n state = state ? 0 : 1;\n context.set(\"state\", state);\n\n const template = context.get(\"template\") || {};\n const out = Object.assign({}, template, { payload: state });\n\n node.send(out);\n\n // Schedule next emission\n const delay = decideNextDelayMs();\n const t = setTimeout(emitTick, delay);\n context.set(\"timer\", t);\n}\n\nif (!running) {\n context.set(\"running\", true);\n node.status({ fill: \"green\", shape: \"dot\", text: \"started\" });\n}\n\n// Don’t start multiple timers if you accidentally keep the old repeating inject\n// before\n// after — always start fresh on trigger; ignore stale post-deploy refs\nclearTimeout(context.get(\"timer\"));\ncontext.set(\"timer\", setTimeout(emitTick, 100));\n\nreturn null;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1440,"y":240,"wires":[["ad2b65978bbb790a"]]},{"id":"34fe41a1d67fb9cd","type":"function","z":"8ccf34b55a2afcad","name":"Persist pairing config","func":"const result = msg.payload || {};\nif (!result.ok) return null;\n\nconst config = global.get(\"config\") || {};\nif (!config.machineId || !config.apiKey) return null;\n\nconst settings = global.get(\"settings\") || {};\nconst shifts = settings.shifts || [{ start: \"08:00\", end: \"16:00\" }];\n\nmsg.topic = `\nINSERT INTO current_config (\n machine_id,\n api_key,\n org_id,\n cloud_base_url,\n shifts_json,\n shift_change_comp_min,\n lunch_break_min,\n threshold_multiplier,\n oee_alert_threshold,\n mold_total,\n mold_active,\n updated_at\n) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())\nON DUPLICATE KEY UPDATE\n api_key=VALUES(api_key),\n org_id=VALUES(org_id),\n cloud_base_url=VALUES(cloud_base_url),\n updated_at=NOW();\n`.trim();\n\nmsg.payload = [\n config.machineId,\n config.apiKey,\n config.orgId || null,\n config.cloudBaseUrl || null,\n JSON.stringify(shifts),\n Number(settings.shiftChangeCompensation ?? 10),\n Number(settings.lunchBreakMinutes ?? 30),\n Number(settings.thresholdMultiplier ?? 1.5),\n Number(settings.oeeAlertThreshold ?? 90),\n Number(settings.moldTotal ?? 0),\n Number(settings.moldActive ?? 0)\n];\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1000,"y":1060,"wires":[["79b190464e080076","fc335baf28ecf342"]]},{"id":"79b190464e080076","type":"mysql","z":"8ccf34b55a2afcad","mydb":"fc9634aabefee16b","name":"Mold Presets DB","x":1230,"y":1060,"wires":[["809c53d80b2a3f11"]]},{"id":"8a1e0fb8d6145ea2","type":"mysql","z":"8ccf34b55a2afcad","mydb":"fc9634aabefee16b","name":"Mold Presets DB","x":3850,"y":220,"wires":[["4130b31ead5d77cb","6f8ad0a90b896c34"]]},{"id":"8bc52997ba90bd62","type":"function","z":"8ccf34b55a2afcad","name":"load config from DB","func":"msg.topic = `\nSELECT machine_id, api_key, org_id, cloud_base_url\nFROM current_config\nWHERE machine_id IS NOT NULL AND machine_id <> '' AND api_key IS NOT NULL AND api_key <> ''\nORDER BY updated_at DESC\nLIMIT 1;\n`.trim();\nmsg.payload = [];\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":3650,"y":220,"wires":[["8a1e0fb8d6145ea2"]]},{"id":"4130b31ead5d77cb","type":"function","z":"8ccf34b55a2afcad","name":"apply config from DB","func":"const rows = msg.payload;\n\nif (!Array.isArray(rows) || rows.length === 0) {\n msg._configRetry = (msg._configRetry || 0) + 1;\n msg._configLoaded = false;\n return [null, msg]; // retry path\n}\n\nconst row = rows[0] || {};\nconst machineId = (row.machine_id || \"\").toString().trim();\nconst apiKey = (row.api_key || \"\").toString().trim();\n\nif (!machineId || !apiKey) {\n msg._configRetry = (msg._configRetry || 0) + 1;\n msg._configLoaded = false;\n return [null, msg]; // retry path\n}\n\nif (msg._configRetry > 12) return [null, null]; // optional stop after 12 tries\n\nconst config = global.get(\"config\") || {};\nconfig.machineId = machineId;\nconfig.apiKey = apiKey;\nif (row.org_id) config.orgId = row.org_id;\nif (row.cloud_base_url) config.cloudBaseUrl = row.cloud_base_url;\nif (config.settingsReadOnly === undefined) config.settingsReadOnly = true;\nif (!config.mqttTopicPrefix) config.mqttTopicPrefix = \"mis\";\n\nglobal.set(\"config\", config);\nmsg._configLoaded = true;\nreturn [msg, null];\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":4080,"y":220,"wires":[[],["306a6b6cc0df4564"]]},{"id":"5452d6525c3ff5a6","type":"function","z":"8ccf34b55a2afcad","d":true,"name":"throttle + dedupe","func":"// KPI Gate: 1/min throttle + dedupe + fresh timestamp\nconst now = Date.now();\nconst minIntervalMs = 60000;\n\nconst payload = msg.outbox?.payload;\nif (!payload) return null;\n\n// stable signature (no tsMs)\nconst sig = JSON.stringify({\n workOrderId: payload.activeWorkOrder?.id || null,\n cycle_count: payload.cycle_count || 0,\n good_parts: payload.good_parts || 0,\n scrap_parts: payload.scrap_parts || 0,\n cavities: payload.cavities || null,\n cycleTime: payload.cycleTime || 0,\n actualCycleTime: payload.actualCycleTime || 0,\n trackingEnabled: !!payload.trackingEnabled,\n productionStarted: !!payload.productionStarted,\n kpis: payload.kpis || {},\n});\n\nconst lastSentAt = flow.get(\"kpi_lastSentAt\") || 0;\nconst lastSig = flow.get(\"kpi_lastSig\") || \"\";\n\n// Drop if too soon and unchanged\nif ((now - lastSentAt) < minIntervalMs && sig === lastSig) {\n return null;\n}\n\n// Update gate state\nflow.set(\"kpi_lastSentAt\", now);\nflow.set(\"kpi_lastSig\", sig);\n\n// Ensure fresh timestamp to avoid timeline weirdness\npayload.tsMs = now;\nmsg.outbox.payload = payload;\nmsg.tsMs = now;\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2410,"y":1080,"wires":[[]]},{"id":"87158a0fe2a0f7af","type":"inject","z":"8ccf34b55a2afcad","name":"KPI minute tick","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":2520,"y":840,"wires":[["de4db4c8eec41d88"]]},{"id":"de4db4c8eec41d88","type":"function","z":"8ccf34b55a2afcad","name":"Build KPI Minute Snapshot","func":"// Build KPI Minute Snapshot (single source of KPI outbox)\nconst config = global.get(\"config\") || {};\nconst state = global.get(\"state\") || {};\nconst machineId = config.machineId;\nif (!machineId) return null;\n\nconst now = Date.now();\n// Optional: align to minute boundary for clean timeline\nconst tsMs = now - (now % 60000);\n\nconst rawActive = state.activeWorkOrder || null;\nconst active = rawActive ? { ...rawActive } : null;\n\n// Normalize activeWorkOrder.lastUpdateIso to string if present\nif (active && active.lastUpdateIso != null) {\n const v = active.lastUpdateIso;\n if (typeof v === \"string\") {\n // ok\n } else if (v instanceof Date) {\n active.lastUpdateIso = v.toISOString();\n } else if (typeof v === \"number\") {\n active.lastUpdateIso = new Date(v).toISOString();\n } else if (v && typeof v.toISOString === \"function\") {\n active.lastUpdateIso = v.toISOString();\n } else if (v && typeof v.toISO === \"function\") {\n active.lastUpdateIso = v.toISO();\n } else if (v && typeof v.format === \"function\") {\n active.lastUpdateIso = v.format();\n } else {\n delete active.lastUpdateIso; // avoid invalid payload\n }\n}\n\nconst kpis = state.currentKPIs || { oee: 0, availability: 0, performance: 0, quality: 0 };\n\nconst cavitiesRaw = Number(\n active?.cavities ??\n state.lastMoldActive ??\n 0\n);\nconst cavities = cavitiesRaw > 0 ? cavitiesRaw : null;\n\nmsg.machineId = machineId;\nmsg.tsMs = tsMs;\n\nmsg.outbox = {\n type: \"kpi\",\n payload: {\n tsMs,\n activeWorkOrder: active,\n cycle_count: Number(state.cycleCount ?? 0),\n good_parts: Number(active?.goodParts ?? 0),\n scrap_parts: Number(active?.scrapParts ?? 0),\n cavities,\n cycleTime: Number(active?.cycleTime ?? state.lastCycleTime ?? 0),\n actualCycleTime: Number(state.lastActualCycleTime ?? 0),\n trackingEnabled: !!state.trackingEnabled,\n productionStarted: !!state.productionStarted,\n kpis,\n },\n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2740,"y":840,"wires":[["4e12fb078508f92e"]]},{"id":"a9fa224f48b83f71","type":"delay","z":"8ccf34b55a2afcad","name":"","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":3460,"y":220,"wires":[["8bc52997ba90bd62"]]},{"id":"306a6b6cc0df4564","type":"switch","z":"8ccf34b55a2afcad","name":"","property":"config.apiKey","propertyType":"global","rules":[{"t":"nempty"},{"t":"empty"}],"checkall":"true","repair":false,"outputs":2,"x":4290,"y":220,"wires":[[],["a9fa224f48b83f71"]]},{"id":"fc335baf28ecf342","type":"debug","z":"8ccf34b55a2afcad","name":"debug 12","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1210,"y":1160,"wires":[]},{"id":"809c53d80b2a3f11","type":"debug","z":"8ccf34b55a2afcad","name":"debug 27","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1050,"y":1240,"wires":[]},{"id":"6f8ad0a90b896c34","type":"debug","z":"8ccf34b55a2afcad","name":"debug 13","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":4030,"y":140,"wires":[]},{"id":"ad2b65978bbb790a","type":"debug","z":"8ccf34b55a2afcad","name":"debug 14","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2710,"y":460,"wires":[]},{"id":"aa698e63d1f67845","type":"debug","z":"8ccf34b55a2afcad","name":"debug 23","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2310,"y":60,"wires":[]},{"id":"467cfdec8cf7257d","type":"16inpind","z":"8ccf34b55a2afcad","name":"","stack":"0","channel":"5","x":2740,"y":120,"wires":[["9a2690258e151b66"]]},{"id":"21b91336f4d77c41","type":"inject","z":"8ccf34b55a2afcad","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"0.01","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":2550,"y":120,"wires":[["467cfdec8cf7257d"]]},{"id":"919b5b8d778e2b6c","type":"ui_group","name":"Default","tab":"c567195d86466cd5","order":1,"disp":false,"width":"25","collapse":false,"className":""},{"id":"e2f3a4b5c6d7e8f9","type":"ui_group","name":"Alerts Group","tab":"a1b2c3d4e5f60718","order":1,"disp":false,"width":"25","collapse":false,"className":""},{"id":"e3f4a5b6c7d8e9f0","type":"ui_group","name":"Graphs Group","tab":"b2c3d4e5f6a70182","order":1,"disp":false,"width":"25","collapse":false,"className":""},{"id":"e4f5a6b7c8d9e0f1","type":"ui_group","name":"Help Group","tab":"c3d4e5f6a7b80192","order":1,"disp":false,"width":"25","collapse":false,"className":""},{"id":"e5f6a7b8c9d0e1f2","type":"ui_group","name":"Settings Group","tab":"d4e5f6a7b8c90123","order":1,"disp":false,"width":"25","collapse":false,"className":""},{"id":"b99c269687d574aa","type":"ui_group","name":"Work Orders Group","tab":"d1a1e2f3a4b5c6d7","order":1,"disp":false,"width":25,"collapse":false,"className":""},{"id":"fc9634aabefee16b","type":"MySQLdatabase","name":"Edge Outbox","host":"127.0.0.1","port":"3306","db":"edge_outbox","tz":"","charset":"UTF8"},{"id":"c567195d86466cd5","type":"ui_tab","name":"Home","icon":"dashboard","order":1,"disabled":false,"hidden":false},{"id":"a1b2c3d4e5f60718","type":"ui_tab","name":"Alerts","icon":"warning","order":3,"disabled":false,"hidden":false},{"id":"b2c3d4e5f6a70182","type":"ui_tab","name":"Graphs","icon":"show_chart","order":4,"disabled":false,"hidden":false},{"id":"c3d4e5f6a7b80192","type":"ui_tab","name":"Help","icon":"help","order":5,"disabled":false,"hidden":false},{"id":"d4e5f6a7b8c90123","type":"ui_tab","name":"Settings","icon":"settings","order":6,"disabled":false,"hidden":false},{"id":"d1a1e2f3a4b5c6d7","type":"ui_tab","name":"Work Orders","icon":"list","order":2,"disabled":false,"hidden":false},{"id":"43d51056507542b5","type":"global-config","env":[],"modules":{"node-red-node-mysql":"2.0.0","node-red-dashboard":"3.6.6","node-red-contrib-spreadsheet-in":"0.7.2","node-red-node-pi-gpio":"2.0.6","node-red-contrib-sm-16inpind":"1.0.1"}}] \ No newline at end of file +[ + { + "id": "c19582f27bf28841", + "type": "subflow", + "name": "Outbox Enqueue v1 (1) (4) (2) (3) (1) (3)", + "info": "", + "category": "", + "in": [ + { + "x": 40, + "y": 40, + "wires": [ + { + "id": "b2bc325b50fc6722" + } + ] + } + ], + "out": [ + { + "x": 1160, + "y": 40, + "wires": [ + { + "id": "f9f048e59b5d41a9", + "port": 0 + } + ] + } + ], + "env": [], + "meta": {}, + "color": "#DDAA99" + }, + { + "id": "b2bc325b50fc6722", + "type": "function", + "z": "c19582f27bf28841", + "name": "Prepare + Validate + Call next_seq", + "func": "// Outbox Enqueue v1 - Step 1\nconst config = global.get(\"config\") || {};\nconst out = msg.outbox || {};\nconst type = out.type;\nconst payload = out.payload;\n\nif (!type) throw new Error(\"Outbox Enqueue: missing msg.outbox.type\");\nif (!payload || typeof payload !== \"object\") throw new Error(\"Outbox Enqueue: missing/invalid msg.outbox.payload\");\n\n// Where to send later (Publisher will use this)\nconst endpointByType = {\n cycle: \"/api/ingest/cycle\",\n event: \"/api/ingest/event\",\n kpi: \"/api/ingest/kpi\",\n heartbeat: \"/api/ingest/heartbeat\",\n segment: \"/api/ingest/segment\", // future; ok to queue now even if not used yet\n};\n\nconst endpoint = out.endpoint || endpointByType[type];\nif (!endpoint) throw new Error(`Outbox Enqueue: unknown type '${type}' and no endpoint provided`);\n\n// Get machineId from msg or global config\nconst machineId = msg.machineId || config.machineId;\nif (!machineId) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Outbox waiting for pairing\" });\n return null;\n}\n\nconst schemaVersion = msg.schemaVersion || \"1.0\";\nconst tsMs = typeof msg.tsMs === \"number\" ? msg.tsMs : Date.now();\n\n// Stash meta for later nodes\nmsg._outboxMeta = { type, endpoint, machineId, schemaVersion, tsMs, payload };\n\nconst validators = {\n cycle: (p) => p && typeof p.cycle === \"object\",\n event: (p) => p && typeof p.event === \"object\",\n kpi: (p) => p && p.kpis && typeof p.kpis === \"object\",\n heartbeat: (p) => p && typeof p.status === \"string\",\n segment: (p) => p && typeof p === \"object\",\n};\n\nconst validator = validators[type];\nif (!validator || !validator(payload)) {\n node.warn(`Outbox Enqueue: invalid ${type} payload`);\n return null;\n}\n\n// Call stored procedure to get next seq (safe + persistent)\nmsg.topic = \"CALL next_seq(?);\";\nmsg.payload = [machineId];\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 220, + "y": 40, + "wires": [ + [ + "284de04f3e1dd8c7" + ] + ] + }, + { + "id": "284de04f3e1dd8c7", + "type": "mysql", + "z": "c19582f27bf28841", + "mydb": "fc9634aabefee16b", + "name": "CALL next_seq", + "x": 460, + "y": 40, + "wires": [ + [ + "5a555283b4a45c58" + ] + ] + }, + { + "id": "5a555283b4a45c58", + "type": "function", + "z": "c19582f27bf28841", + "name": "Build envelope + prepare INSERT", + "func": "// Outbox Enqueue v1 - Step 2\nconst meta = msg._outboxMeta;\nif (!meta) throw new Error(\"Outbox Enqueue: missing _outboxMeta\");\n\nfunction stripNil(obj) {\n if (!obj || typeof obj !== \"object\") return obj;\n const out = Array.isArray(obj) ? [] : {};\n for (const [k, v] of Object.entries(obj)) {\n if (v === undefined || v === null) continue; // <— key change\n out[k] = (v && typeof v === \"object\") ? stripNil(v) : v;\n }\n return out;\n}\n\n// Parse seq from MySQL result\n// mysql node often returns an array of result sets for CALL\nlet seq = null;\nconst res = msg.payload;\n\nif (Array.isArray(res)) {\n // common shape: [ [ { seq: 123 } ], ... ]\n if (Array.isArray(res[0]) && res[0][0] && res[0][0].seq != null) seq = res[0][0].seq;\n // sometimes: [ { seq: 123 } ]\n if (seq == null && res[0] && res[0].seq != null) seq = res[0].seq;\n}\n\nif (seq == null) throw new Error(\"Outbox Enqueue: could not parse seq from DB result\");\n\n\n\n\nfunction normalizeIsoDeep(obj) {\n if (!obj || typeof obj !== \"object\") return obj;\n if (Array.isArray(obj)) return obj.map(normalizeIsoDeep);\n\n for (const k of Object.keys(obj)) {\n const v = obj[k];\n\n // recurse first\n if (v && typeof v === \"object\") obj[k] = normalizeIsoDeep(v);\n\n // ISO fields: must be string|null\n if (k.toLowerCase().endsWith(\"iso\")) {\n const vv = obj[k];\n if (typeof vv === \"string\") continue;\n if (typeof vv === \"number\") obj[k] = new Date(vv).toISOString();\n else if (vv == null) obj[k] = null;\n else obj[k] = null; // <- kills {} forever\n }\n }\n return obj;\n}\n\n\n\n\n\nconst cleanPayload = normalizeIsoDeep(stripNil(meta.payload));\nconst envelope = {\n schemaVersion: meta.schemaVersion,\n machineId: meta.machineId,\n tsMs: meta.tsMs,\n seq: String(seq),\n type: meta.type,\n payload: cleanPayload\n};\n\n\n// Prepare insert into outbox_messages\nmsg.topic = `\nINSERT INTO outbox_messages\n(machine_id, msg_type, endpoint, schema_version, seq, ts_device_ms, payload_json, status, attempts, next_attempt_at)\nVALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, NULL);\n`.trim();\n\nmsg.payload = [\n meta.machineId,\n meta.type,\n meta.endpoint,\n meta.schemaVersion,\n Number(seq), // BIGINT\n meta.tsMs, // BIGINT\n JSON.stringify(envelope),\n];\n\n// Keep a copy for debugging\nmsg._envelope = envelope;\nmsg._seq = Number(seq);\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 720, + "y": 40, + "wires": [ + [ + "f9f048e59b5d41a9" + ] + ] + }, + { + "id": "f9f048e59b5d41a9", + "type": "mysql", + "z": "c19582f27bf28841", + "mydb": "fc9634aabefee16b", + "name": "Insert outbox_messages", + "x": 1010, + "y": 40, + "wires": [ + [] + ] + }, + { + "id": "8ccf34b55a2afcad", + "type": "tab", + "label": "Flow 2.1", + "disabled": false, + "info": "", + "env": [] + }, + { + "id": "c95c05b78d8464c5", + "type": "group", + "z": "8ccf34b55a2afcad", + "name": "Start ", + "style": { + "stroke": "#92d04f", + "fill": "#addb7b", + "label": true + }, + "nodes": [ + "4319d292bf565c9c", + "d29b1f289eeaea5d", + "ac25819b6b50ab90", + "0319de4ecd45bdbf", + "cf7c59785d89fa2a", + "c72da0d5e173ccd5", + "fddb2e595128c225", + "6790210592277c61" + ], + "x": 1134, + "y": 139, + "w": 942, + "h": 142 + }, + { + "id": "892a1a691044805e", + "type": "group", + "z": "8ccf34b55a2afcad", + "name": "Cavity Settings ", + "style": { + "stroke": "#ff7f7f", + "fill": "#ffbfbf", + "label": true + }, + "nodes": [ + "d0779b8395bfabb2" + ], + "x": 122, + "y": 507, + "w": 926, + "h": 306 + }, + { + "id": "472f828e204736a6", + "type": "group", + "z": "8ccf34b55a2afcad", + "name": "UI/UX", + "style": { + "fill": "#d1d1d1", + "label": true + }, + "nodes": [ + "87dc48bbe753ab8d", + "2221910b5a895480", + "6feab0f06c780f4c", + "f4038d2505b262ba", + "372edd599e1b2fdd", + "405dccd3e09a99d4", + "6adb9fd7e98f4274", + "7de72e0542b98762", + "7b07faa72600f3fd", + "af1c33999012b1c1", + "c7a8bb7adab6838e", + "8f16fdca6d059017", + "47a6ea4de26fadce", + "de82c1e664a73bfb", + "a51caffa7492c805", + "59c4e2cd83fd1876", + "2139786b0733335d", + "5eeeac992826fa3b", + "3dd6baf608946fde", + "d7603f458aa33b0d", + "799fcf989e49f86a", + "5a1b8c2451faa879" + ], + "x": 214, + "y": 179, + "w": 672, + "h": 362 + }, + { + "id": "1d1ce0cb54c52345", + "type": "group", + "z": "8ccf34b55a2afcad", + "name": "Work Orders", + "style": { + "stroke": "#9363b7", + "fill": "#dbcbe7", + "label": true + }, + "nodes": [ + "0fe0cdc07941b31e", + "8fe1661536e76e10", + "65c3c2889a068c25", + "5df1ee6ee335a44a", + "8abddc40064f1f64", + "c48444f30fc71bae", + "e5272e3e630666ce", + "4a382caf6dadab4c", + "fc87eda3d68142da", + "8dcfbe64c30024e6", + "86abdbe356524296", + "07b66a930e3e5e40", + "ddf6d786b5a7e682", + "1e8c2370ba268726", + "35542f4dc31c3a96", + "1af33da0fece65a7", + "7cb1c8d40f510e0b", + "6f993b8cb99de446", + "2d893a89211ae18f", + "2872629594daf0ed", + "037e4825e2537ed2", + "16f0e85a7bc88164", + "a75b33fb4e9ebdf8", + "30be157b60fc55fd", + "a0a8bda316e17f8e", + "97399876ca829d1a", + "cb972412abfe909f", + "04accd50a0fe3c90", + "ca6e4da8d0c5f853", + "ea175b8208d85ed5", + "d116553e03c9a47c", + "1a14505b6501d2dc", + "286df6ee52e241ca", + "af0fb4f5a75407ac", + "c323fd9c710bcded", + "21e9ba4f889da77f", + "4bb96d61f9e3bba2", + "cc02262fda7af805" + ], + "x": 1034, + "y": 279, + "w": 1432, + "h": 682 + }, + { + "id": "40295dcf4b5e1779", + "type": "group", + "z": "8ccf34b55a2afcad", + "name": "Anomaly System", + "style": { + "stroke": "#ffC000", + "fill": "#ffdf7f", + "label": true + }, + "nodes": [ + "25e40f96b0876550", + "aa9948b7e5229b0d", + "2dffe9379e43af37", + "184c513e8d1389dd", + "d4ad56bfc7f333ba" + ], + "x": 224, + "y": 19, + "w": 922, + "h": 102 + }, + { + "id": "1f2e45c551f40615", + "type": "group", + "z": "8ccf34b55a2afcad", + "name": "Alerts", + "style": { + "fill": "#bfdbef", + "label": true + }, + "nodes": [ + "851a0e837a94ed78", + "6f2897368cb48370", + "154c7f1ad173419e" + ], + "x": 234, + "y": 819, + "w": 512, + "h": 82 + }, + { + "id": "8b73333804b30ee0", + "type": "group", + "z": "8ccf34b55a2afcad", + "name": "Graphs", + "style": { + "fill": "#bfbfbf", + "label": true + }, + "nodes": [ + "2848a91e7a88cd0b", + "52501193c1677944", + "429b86c710e30c33", + "89d2d406e3cd96ee", + "9d321f6ae2618e66" + ], + "x": 1194, + "y": 39, + "w": 802, + "h": 82 + }, + { + "id": "d0779b8395bfabb2", + "type": "group", + "z": "8ccf34b55a2afcad", + "g": "892a1a691044805e", + "name": "Cavities Settings", + "style": { + "stroke": "#ffff00", + "fill": "#ffffbf", + "label": true + }, + "nodes": [ + "c04e913f614d9036" + ], + "x": 148, + "y": 533, + "w": 874, + "h": 254 + }, + { + "id": "c04e913f614d9036", + "type": "group", + "z": "8ccf34b55a2afcad", + "g": "d0779b8395bfabb2", + "name": "Settings", + "style": { + "stroke": "#92d04f", + "fill": "#ffffbf", + "label": true + }, + "nodes": [ + "746bd5f6d88850e7", + "2c31593fa4f5fbaa", + "70860403340a5bc0", + "69540565a3ee537b", + "183adc7ca661960e", + "6b1a37faf14b1a55", + "f1a89a5092479152", + "b45e6bfabde1f79c" + ], + "x": 174, + "y": 559, + "w": 822, + "h": 202 + }, + { + "id": "87dc48bbe753ab8d", + "type": "ui_template", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "group": "919b5b8d778e2b6c", + "name": "Home Template", + "order": 0, + "width": "25", + "height": "25", + "format": "\n
\n \n\n
\n
\n
\n
\n
OEE
\n
0%
\n
\n
\n
Disponibilidad
\n
0%
\n
\n
\n
Rendimiento
\n
0%
\n
\n
\n
Calidad
\n
0%
\n
\n
\n\n
\n

Orden de trabajo actual

\n
\n
\n
ID Orden trabajo
\n
 
\n
\n
\n
SKU
\n
 
\n
\n
\n
Tiempo de ciclo
\n
0
\n
\n
\n
\n
\n
0%
\n
\n
\n\n
\n
\n
Piezas buenas
\n
0
\n
de 0
\n
\n\n
\n
MaquinaOFFLINE
\n
ProducciónDETENIDA
\n
\n\n \n
\n Cambio de molde en curso desde {{ moldChange.startMs | date:'HH:mm' }} · {{ moldChangeElapsedMin() }} min\n
\n
\n \n
\n\n
\n
\n \n\n\n\n\n \n\n
\n
\n\n\n
\n
\n

Orden de trabajo en proceso

\n

{{ resumePrompt.id }}

\n

\n {{ resumePrompt.goodParts }} of {{ resumePrompt.targetQty }} partes completadas\n ({{ resumePrompt.progressPercent }}%)\n

\n

Ciclos: {{ resumePrompt.cycleCount }}

\n\n
\n \n \n
\n
\n
\n
\n
\n

{{ scrapPrompt.title || 'Orden de trabajo completada' }}

\n

{{ scrapPrompt.orderId }}

\n

\n Producido {{ scrapPrompt.produced }} de {{ scrapPrompt.target }} piezas\n

\n\n

\n Scrap acumulado: {{ scrapPrompt.scrapSoFar }}\n

\n\n
\n {{ scrapPrompt.manual ? '¿Cuántas piezas de scrap quieres agregar ahora?' : 'Hubo piezas de scrap?' }}\n
\n\n \n
\n
{{ scrapPrompt.scrapCount || 0 }}
\n
{{ scrapPrompt.error }}
\n\n
\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n
\n\n
\n \n
\n
\n\n \n
\n \n
\n \n \n
\n\n \n \n
\n
\n
\n\n
\n
\n

Selecciona razón de scrap

\n
\n Paso {{ scrapReasonPrompt.step }} de 2\n | {{ scrapReasonPrompt.selectedCategory.label }}\n
\n\n
\n \n
\n\n
\n \n
\n\n
\n \n \n
\n
\n
\n\n\n\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 380, + "y": 280, + "wires": [ + [ + "6adb9fd7e98f4274", + "c7a8bb7adab6838e", + "c3ef5e9b98a8e24f" + ] + ] + }, + { + "id": "2221910b5a895480", + "type": "ui_template", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "group": "e2f3a4b5c6d7e8f9", + "name": "Alerts Template", + "order": 0, + "width": "25", + "height": "25", + "format": "\n
\n \n\n
\n
\n
\n

Incidentes

\n
\n\n
\n \n \n \n
\n\n
\n
\n \n \n
\n
\n \n \n
\n \n
\n
\n
\n
\n\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 380, + "y": 360, + "wires": [ + [ + "6adb9fd7e98f4274", + "7a1de7abad9506ec" + ] + ] + }, + { + "id": "6feab0f06c780f4c", + "type": "ui_template", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "group": "e3f4a5b6c7d8e9f0", + "name": "Graphs Template", + "order": 0, + "width": "25", + "height": "25", + "format": "\n\n
\n \n\n
\n
\n

Graphs

\n\n\n
\n
\n

OEE

\n
\n
\n\n
\n

Disponibilidad

\n
\n
\n\n
\n

Rendimiendo

\n
\n
\n\n
\n

Calidad

\n
\n
\n
\n
\n
\n
\n\n\n\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 390, + "y": 400, + "wires": [ + [ + "6adb9fd7e98f4274" + ] + ] + }, + { + "id": "f4038d2505b262ba", + "type": "ui_template", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "group": "e4f5a6b7c8d9e0f1", + "name": "Help Template", + "order": 0, + "width": "25", + "height": "25", + "format": "\n
\n \n\n
\n
\n
\n

Help

\n
\n\n
\n

Acerca de este panel

\n

Esta interfaz monitorea los indicadores de Eficiencia Global del Equipo (OEE), el avance de producción y el registro de incidentes para operaciones de inyección de plástico. Navega entre las pestañas usando la barra lateral para acceder a órdenes de trabajo, monitoreo en tiempo real, gráficas de desempeño, registro de incidentes y configuración de máquina.

\n
\n\n
\n

Cómo empezar con una orden de trabajo

\n

Ve a la pestaña Órdenes de Trabajo y carga un archivo de Excel con tus órdenes, o selecciona una orden existente en la tabla. Haz clic en Cargar para activar una orden de trabajo y luego navega a la pestaña Inicio, donde verás los detalles de la orden actual. Antes de iniciar producción, configura tu molde en la pestaña Configuración, seleccionando un preset de molde o ingresando manualmente el número de cavidades.

\n
\n\n
\n

Ejecutando producción

\n

En la pestaña Inicio, presiona el botón INICIAR para comenzar la producción. El sistema registra conteo de ciclos, piezas buenas, scrap y el avance hacia tu cantidad objetivo. Presiona DETENER para pausar la producción. Monitorea en tiempo real los KPIs, incluyendo OEE, Disponibilidad, Rendimiento y Calidad mostrados en el tablero.

\n
\n\n
\n

Registro de incidentes

\n

Usa la pestaña Alertas para registrar incidentes de producción. Registra rápidamente problemas comunes con botones preconfigurados como Falta de Material, Máquina Detenida o Paro de Emergencia. Para un registro más detallado, selecciona un tipo de alerta en el menú desplegable, agrega notas y envía. Todos los incidentes se registran con sello de tiempo y se utilizan en los cálculos de Disponibilidad y OEE.

\n
\n\n
\n

Configuración de moldes

\n

En la pestaña Configuración, utiliza la sección de Preconfigurados de Molde para buscar tu molde por fabricante y nombre. Selecciona una configuración para cargar automáticamente el número de cavidades, o ajusta manualmente los campos de Configuración de Molde. Si tu molde no aparece, usa el botón Agregar Molde en la sección de Integraciones para crear un nuevo preset con fabricante, nombre y detalles de cavidades.

\n
\n\n
\n

Visualización de datos de desempeño

\n

La pestaña Gráficas muestra el historial de tendencias de OEE desglosado por Disponibilidad, Desempeño y Calidad. Usa estas gráficas para identificar patrones, dar seguimiento a mejoras y diagnosticar problemas recurrentes que afecten la eficiencia de tu producción

\n
\n
\n
\n
\n\n\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 380, + "y": 440, + "wires": [ + [ + "6adb9fd7e98f4274" + ] + ] + }, + { + "id": "372edd599e1b2fdd", + "type": "ui_template", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "group": "e5f6a7b8c9d0e1f2", + "name": "Settings Template", + "order": 0, + "width": "25", + "height": "25", + "format": "\n
\n \n\n
\n
\n
\n

Ajustes

\n
\n\n
\n

Moldes Preconfigurados

\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n
\n \n
\n

Seleccionar un fabricante y el molde de las siguientes opciones.

\n
\n\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n \n \n
\n
\n
\n
\n\n
\n

Configuraciones de Molde

\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n\n
\n

Integraciones

\n
\n
\n \n
\n
\n

No encuentras lo que buscas?

\n \n
\n
\n
\n
\n

Enlace Control Tower

\n

Ingresa el codigo de 5 caracteres que aparece en Control Tower para enlazar esta Raspberry Pi.

\n
\n
\n \n \n
\n
\n \n
\n
\n

{{ pairingStatus }}

\n
\n
\n

WiFi

\n

Conecta esta Raspberry Pi a una red WiFi (sin salir del dashboard).

\n \n
\n
\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n \n \n
\n \n
\n \n \n
\n
\n \n
\n \n \n \n \n \n
\n \n

\n {{ wifiStatusText }}\n

\n
\n
\n

Programación de Producción

\n\n \n
\n
\n \n
\n
\n\n
\n
\n \n \n
\n
\n \n \n
\n
1\">\n \n
\n
\n\n \n\n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n \n \n {{ saveStatus }}\n \n
\n\n
\n

Umbral OEE

\n
\n
\n \n \n

\n Gap > (Cycle Time × {{thresholdMultiplier || 1.5}}) = Stoppage\n

\n
\n
\n \n \n
\n
\n
\n
\n
\n
\n\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 390, + "y": 480, + "wires": [ + [ + "6adb9fd7e98f4274", + "af1c33999012b1c1", + "3dd6baf608946fde" + ] + ] + }, + { + "id": "405dccd3e09a99d4", + "type": "ui_template", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "group": "b99c269687d574aa", + "name": "WO Template", + "order": 1, + "width": "25", + "height": "25", + "format": "\n
\n \n\n
\n
\n
\n

Ordenes de trabajo

\n
\n \n 0 selected\n \n \n \n \n \n \n \n
\n
\n\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
IDSKUMETABUENASSCRAPPROGRESOSTATUSULTIMA ACTUALIZACIÓN
\n
\n
\n 0 items\n Tip: Dejale picado para seleccionar multiples ordenes\n
\n
\n
\n
\n
\n\n\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 380, + "y": 320, + "wires": [ + [ + "6adb9fd7e98f4274", + "c7a8bb7adab6838e" + ] + ] + }, + { + "id": "6adb9fd7e98f4274", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "Tab navigation", + "func": "if (msg.ui_control && msg.ui_control.tab) {\n msg.payload = { tab: msg.ui_control.tab };\n delete msg.ui_control;\n return msg;\n}\nreturn null;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 620, + "y": 380, + "wires": [ + [ + "7de72e0542b98762" + ] + ] + }, + { + "id": "7de72e0542b98762", + "type": "ui_ui_control", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "ui_control", + "events": "all", + "x": 800, + "y": 380, + "wires": [ + [] + ] + }, + { + "id": "7b07faa72600f3fd", + "type": "ui_template", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "group": "", + "name": "General Style", + "order": 0, + "width": 0, + "height": 0, + "format": "", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "global", + "className": "", + "x": 760, + "y": 300, + "wires": [ + [] + ] + }, + { + "id": "25e40f96b0876550", + "type": "ui_template", + "z": "8ccf34b55a2afcad", + "g": "40295dcf4b5e1779", + "group": "919b5b8d778e2b6c", + "name": "Anomaly Alert System (Global)", + "order": 1, + "width": "25", + "height": "25", + "format": "\n\n\n\n\n\n
\n
\n

Active Alerts

\n \n
\n\n
\n
\n
\n

No active alerts

\n

All systems operating normally

\n
\n\n
\n
\n

{{anomaly.title}}

\n {{anomaly.severity}}\n
\n

{{anomaly.description}}

\n

{{formatTimestamp(anomaly.tsMs)}}

\n
\n \n
\n
\n
\n
\n\n\n
\n
\n
\n

{{popup.title}}

\n \n
\n

{{popup.description}}

\n \n
\n
\n\n
\n
\n

Selecciona razón de paro

\n

{{reasonPrompt.anomalyTitle || 'Incidente de downtime'}}

\n
\n Paso {{reasonPrompt.step}} de 2\n | {{reasonPrompt.selectedCategory.label}}\n
\n\n
\n \n
\n\n
\n \n
\n\n
\n \n \n
\n
\n
\n\n\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 430, + "y": 60, + "wires": [ + [ + "aa9948b7e5229b0d", + "7bd006e12583f8e1" + ] + ] + }, + { + "id": "aa9948b7e5229b0d", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "40295dcf4b5e1779", + "name": "Handle Anomaly Acknowledgment", + "func": "// Handle anomaly acknowledgments and downtime-reason submit.\n// Output 1: SQL update for anomaly_events\n// Output 2: event payload for Build Event Outbox Payload\nconst anomaly = global.get(\"anomaly\") || {};\nconst topic = msg.topic || \"\";\n\nfunction persistAnomaly(next) {\n global.set(\"anomaly\", next);\n try {\n global.set(\"anomaly\", next, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n}\n\nfunction parseReason(payload) {\n const reasonPath = Array.isArray(payload.reasonPath) ? payload.reasonPath : [];\n const category = reasonPath[0] || {};\n const detail = reasonPath[1] || {};\n return {\n type: payload.reasonType || \"downtime\",\n categoryId: category.id || null,\n categoryLabel: category.label || null,\n detailId: detail.id || null,\n detailLabel: detail.label || null,\n reasonText: payload.reasonText || [\n category.label || \"\",\n detail.label || \"\"\n ].filter(Boolean).join(\" > \") || null,\n catalogVersion: payload.catalogVersion || null,\n incidentKey: payload.incidentKey || payload.incident_key || null\n };\n}\n\nfunction removeFromActive(eventId) {\n let activeAnomalies = anomaly.activeAnomalies || [];\n const idx = activeAnomalies.findIndex((a) => a.event_id === eventId || a.tsMs === eventId);\n if (idx !== -1) {\n activeAnomalies.splice(idx, 1);\n anomaly.activeAnomalies = activeAnomalies;\n }\n}\n\nif (topic === \"acknowledge-anomaly\" || topic === \"anomaly-reason-submit\") {\n const ackData = msg.payload || {};\n const eventId = ackData.event_id ?? ackData.tsMs;\n const ackTimestamp = typeof ackData.tsMs === \"number\" ? ackData.tsMs : Date.now();\n const incidentKey = ackData.incidentKey || ackData.incident_key || null;\n\n if (!eventId) {\n node.warn(\"[ANOMALY ACK] No event_id provided\");\n return null;\n }\n\n if (String(eventId).startsWith(\"temp_\")) {\n removeFromActive(eventId);\n persistAnomaly(anomaly);\n return null;\n }\n\n const sql = (\n \"UPDATE anomaly_events \" +\n \"SET status = 'acknowledged', acknowledged_at = \" + Number(ackTimestamp) + \" \" +\n \"WHERE event_timestamp = \" + Number(eventId)\n );\n const dbMsg = { topic: sql, payload: [] };\n\n removeFromActive(eventId);\n\n if (topic === \"anomaly-reason-submit\") {\n anomaly.reasonsByIncident = anomaly.reasonsByIncident || {};\n anomaly.ackedIncidentKeys = anomaly.ackedIncidentKeys || {};\n\n const reason = parseReason(ackData);\n if (incidentKey) {\n anomaly.reasonsByIncident[incidentKey] = reason;\n anomaly.ackedIncidentKeys[incidentKey] = true;\n }\n\n persistAnomaly(anomaly);\n\n const alreadySent = !!(incidentKey && anomaly.reasonEventsSent && anomaly.reasonEventsSent[incidentKey]);\n if (alreadySent) {\n return [dbMsg, null];\n }\n\n if (incidentKey) {\n anomaly.reasonEventsSent = anomaly.reasonEventsSent || {};\n anomaly.reasonEventsSent[incidentKey] = true;\n persistAnomaly(anomaly);\n }\n\n const eventTsMs = Number(ackData.tsMs) || Date.now();\n const anomalyType = ackData.anomalyType || ackData.anomaly_type || \"downtime\";\n const outEvent = {\n tsMs: eventTsMs,\n eventType: \"downtime-acknowledged\",\n anomalyType,\n eventId: eventId,\n incidentKey: incidentKey || null,\n // Keep reason attached to generic event payload for current ingest.\n reason,\n // Explicit separation for future split in Control Tower.\n downtime: {\n incidentKey: incidentKey || null,\n eventId: eventId,\n anomalyType,\n acknowledgedAtMs: eventTsMs,\n reason\n }\n };\n\n return [{ ...dbMsg }, { payload: outEvent, tsMs: eventTsMs }];\n }\n\n if (incidentKey) {\n anomaly.ackedIncidentKeys = anomaly.ackedIncidentKeys || {};\n anomaly.ackedIncidentKeys[incidentKey] = true;\n }\n persistAnomaly(anomaly);\n\n return [dbMsg, null];\n}\n\nreturn null;\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 720, + "y": 60, + "wires": [ + [ + "2dffe9379e43af37" + ], + [ + "a1e59ef54a91ee50" + ] + ] + }, + { + "id": "4319d292bf565c9c", + "type": "inject", + "z": "8ccf34b55a2afcad", + "g": "c95c05b78d8464c5", + "name": "Simula Inyectora", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "1", + "payloadType": "num", + "x": 1260, + "y": 180, + "wires": [ + [ + "c72da0d5e173ccd5", + "e8efa837400e163b", + "aa698e63d1f67845", + "6790210592277c61" + ] + ] + }, + { + "id": "d29b1f289eeaea5d", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "c95c05b78d8464c5", + "name": "1,0", + "func": "// Get current global value (default to 0 if not set)\nconst state = global.get(\"state\") || {};\nlet estado = state.machineToggle || 0;\nlet stop = flow.get('stop') || false;\n\nif (stop) {\n // Manual stop active → force 0, don't reschedule\n state.machineToggle = 0;\nglobal.set(\"state\", state);\n msg.payload = 0;\n node.send(msg);\n return;\n}\n\n// Toggle between 1 and 0\nestado = estado === 1 ? 0 : 1;\n\n// Update the global variable\nstate.machineToggle = estado;\nglobal.set(\"state\", state);\n\n// Send it out\nmsg.payload = estado;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1670, + "y": 200, + "wires": [ + [ + "ac25819b6b50ab90" + ] + ] + }, + { + "id": "746bd5f6d88850e7", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "name": "Cavities Settings", + "func": "const settings = global.get(\"settings\") || {};\nconst state = global.get(\"state\") || {};\nconst moldByWorkOrder = state.moldByWorkOrder || {};\n\nconst persistSettings = () => {\n global.set(\"settings\", settings);\n try {\n global.set(\"settings\", settings, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst persistMoldCache = () => {\n const cache = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n };\n global.set(\"moldCache\", cache);\n try {\n global.set(\"moldCache\", cache, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst persistState = () => {\n global.set(\"state\", state);\n};\n\nconst persistMoldSelection = (total, active) => {\n if (!Number.isFinite(active) || active <= 0) return;\n state.lastMoldActive = active;\n if (Number.isFinite(total) && total > 0) {\n state.lastMoldTotal = total;\n }\n if (state.activeWorkOrder?.id) {\n state.activeWorkOrder.cavities = active;\n if (Number.isFinite(total) && total > 0) {\n state.activeWorkOrder.cavities_total = total;\n }\n moldByWorkOrder[state.activeWorkOrder.id] = {\n total: Number.isFinite(total) && total > 0 ? total : (state.lastMoldTotal ?? null),\n active\n };\n }\n state.moldByWorkOrder = moldByWorkOrder;\n persistState();\n persistMoldCache();\n};\n\nconst buildCavitiesUpdate = (total, active) => {\n const activeId = state.activeWorkOrder?.id;\n if (!activeId || !Number.isFinite(active) || active <= 0) return null;\n const totalSafe = Number.isFinite(total) && total > 0 ? total : 0;\n return {\n topic: \"UPDATE work_orders SET cavities_total = COALESCE(NULLIF(?,0), cavities_total), cavities_active = COALESCE(NULLIF(?,0), cavities_active), updated_at = NOW() WHERE work_order_id = ?\",\n payload: [totalSafe, active, activeId]\n };\n};\n\nif (msg.topic === \"moldSettings\" && msg.payload) {\n const total = Number(msg.payload.total || 0);\n const active = Number(msg.payload.active || 0);\n\n // Store settings\n settings.moldTotal = total;\n settings.moldActive = active;\n persistSettings();\n\n persistMoldSelection(total, active);\n\n node.status({ fill: \"green\", shape: \"dot\", text: `Saved: ${active}/${total}` });\n\n const dbMsg = buildCavitiesUpdate(total, active);\n\n msg.payload = { saved: true, total, active };\n return [msg, dbMsg];\n}\n\n// Handle preset selection\nif (msg.topic === \"selectMoldPreset\" && msg.payload) {\n const preset = msg.payload;\n const total = Number(preset.theoretical_cavities || 0);\n const active = Number(preset.functional_cavities || 0);\n\n // Store settings\n settings.moldTotal = total;\n settings.moldActive = active;\n persistSettings();\n\n persistMoldSelection(total, active);\n\n node.status({ fill: \"blue\", shape: \"dot\", text: `Preset: ${preset.mold_name}` });\n\n // Send to UI to update fields\n msg.topic = \"moldPresetSelected\";\n msg.payload = { total, active, presetName: preset.mold_name };\n\n const dbMsg = buildCavitiesUpdate(total, active);\n\n return [msg, dbMsg];\n}\n\n//node.status({ fill: \"red\", shape: \"ring\", text: \"Invalid payload\" });\nreturn [null, null];", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 470, + "y": 600, + "wires": [ + [ + "69540565a3ee537b" + ] + ] + }, + { + "id": "2c31593fa4f5fbaa", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "name": "Mold Presets Handler", + "func": "const topic = msg.topic || '';\nconst payload = msg.payload || {};\n\n// ===== IGNORE NON-MOLD TOPICS SILENTLY =====\n// These are KPI/dashboard messages not meant for this handler\nconst ignoredTopics = [\n 'machineStatus',\n 'kpis',\n 'chartsData',\n 'activeWorkOrder',\n 'workOrderCycle',\n 'workOrdersList',\n 'scrapPrompt',\n 'uploadStatus'\n];\n\nif (ignoredTopics.includes(topic) || topic === '') {\n return null; // Silent ignore\n}\n\n// Log only mold-related requests\nnode.warn(`Received: ${topic}`);\n\n// CRITICAL: Use a processing lock to prevent simultaneous requests\nlet dedupeKey = topic;\nif (topic === 'addMoldPreset') {\n dedupeKey = `add_${payload.manufacturer}_${payload.mold_name}`;\n} else if (topic === 'getMoldsByManufacturer') {\n dedupeKey = `getmolds_${payload.manufacturer}`;\n}\n\nconst lockKey = `lock_${dedupeKey}`;\nconst lastRequestKey = `last_request_${dedupeKey}`;\n\n// Check if currently processing this request\nif (flow.get(lockKey) === true) {\n node.warn(`${topic} already processing - duplicate blocked`);\n return null;\n}\n\n// Check timing\nconst now = Date.now();\nconst lastRequestTime = flow.get(lastRequestKey) || 0;\nif (now - lastRequestTime < 2000) {\n node.warn(`Duplicate ${topic} request ignored (within 2s)`);\n return null;\n}\n\n// Set lock IMMEDIATELY before any async operations\nflow.set(lockKey, true);\nflow.set(lastRequestKey, now);\n\n// Release lock after 3 seconds (safety timeout)\nsetTimeout(() => {\n flow.set(lockKey, false);\n}, 3000);\n\n// Load all presets (legacy)\nif (topic === 'loadMoldPresets') {\n msg._originalTopic = 'loadMoldPresets';\n msg.topic = 'SELECT * FROM mold_presets ORDER BY manufacturer, mold_name;';\n node.warn('Querying all presets');\n return msg;\n}\n\n// Search/filter presets (legacy)\nif (topic === 'searchMoldPresets') {\n const filters = msg.payload || {};\n const searchTerm = (filters.searchTerm || '').trim().replace(/['\\\"\\\\\\\\]/g, '');\n const manufacturer = (filters.manufacturer || '').replace(/['\\\"\\\\\\\\]/g, '');\n const theoreticalCavities = filters.theoreticalCavities || '';\n\n let query = 'SELECT * FROM mold_presets WHERE 1=1';\n\n if (searchTerm) {\n const searchPattern = `%${searchTerm}%`;\n query += ` AND (mold_name LIKE '${searchPattern.replace(/'/g, \"''\")}' OR manufacturer LIKE '${searchPattern.replace(/'/g, \"''\")}')`;\n }\n\n if (manufacturer && manufacturer !== 'All') {\n query += ` AND manufacturer = '${manufacturer.replace(/'/g, \"''\")}'`;\n }\n\n if (theoreticalCavities && theoreticalCavities !== '') {\n const cavities = Number(theoreticalCavities);\n if (!isNaN(cavities)) {\n query += ` AND theoretical_cavities = ${cavities}`;\n }\n }\n\n query += ' ORDER BY manufacturer, mold_name;';\n\n msg._originalTopic = 'searchMoldPresets';\n msg.topic = query;\n return msg;\n}\n\n// Get unique manufacturers for dropdown\nif (topic === 'getManufacturers') {\n msg._originalTopic = 'getManufacturers';\n msg.topic = 'SELECT DISTINCT manufacturer FROM mold_presets ORDER BY manufacturer;';\n node.warn('Querying manufacturers');\n return msg;\n}\n\n// Get molds for a specific manufacturer\nif (topic === 'getMoldsByManufacturer') {\n const data = msg.payload || {};\n const manufacturerRaw = (data.manufacturer || '').trim();\n if (!manufacturerRaw) {\n node.warn('No manufacturer provided');\n return null;\n }\n\n const manufacturerSafe = manufacturerRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n\n msg._originalTopic = 'getMoldsByManufacturer';\n msg.topic = `SELECT * FROM mold_presets WHERE manufacturer = '${manufacturerSafe}' ORDER BY mold_name;`;\n node.warn(`Querying molds for: ${manufacturerSafe}`);\n return msg;\n}\n\n// Add a new mold preset - CRITICAL: Strong deduplication\nif (topic === 'addMoldPreset') {\n const data = msg.payload || {};\n const manufacturerRaw = (data.manufacturer || '').trim();\n const moldNameRaw = (data.mold_name || '').trim();\n const theoreticalRaw = (data.theoretical || '').trim();\n const activeRaw = (data.active || '').trim();\n\n if (!manufacturerRaw || !moldNameRaw || !theoreticalRaw || !activeRaw) {\n node.status({ fill: 'red', shape: 'ring', text: 'Missing value' });\n node.warn('Missing required fields');\n return null;\n }\n\n // Additional safety check for already-processed flag\n if (msg._addMoldProcessed) {\n node.warn('addMoldPreset already processed flag detected, ignoring');\n return null;\n }\n msg._addMoldProcessed = true;\n\n const manufacturerSafe = manufacturerRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n const moldNameSafe = moldNameRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n const theoreticalSafe = theoreticalRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n const activeSafe = activeRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n\n msg._originalTopic = 'addMoldPreset';\n msg.topic =\n \"INSERT INTO mold_presets (manufacturer, mold_name, theoretical_cavities, functional_cavities) \" +\n \"VALUES ('\" + manufacturerSafe + \"', '\" + moldNameSafe + \"', \" + theoreticalSafe + \", \" + activeSafe + \");\";\n\n node.status({ fill: 'blue', shape: 'dot', text: 'Inserting mold...' });\n node.warn(`Inserting: ${manufacturerSafe} - ${moldNameSafe}`);\n return msg;\n}\n\nnode.warn(`Unknown topic: ${topic}`);\nreturn null;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 400, + "y": 660, + "wires": [ + [ + "70860403340a5bc0" + ] + ] + }, + { + "id": "70860403340a5bc0", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "mydb": "fc9634aabefee16b", + "name": "Mold Presets DB", + "x": 610, + "y": 660, + "wires": [ + [ + "69540565a3ee537b" + ] + ] + }, + { + "id": "69540565a3ee537b", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "name": "Process DB Results", + "func": "// Replace function in \"Process DB Results\" node\n\nconst originalTopic = msg._originalTopic || '';\nconst dbResults = Array.isArray(msg.payload) ? msg.payload : [];\n\nif (!originalTopic) {\n return null;\n}\n\n// IMPORTANT: Clear socketid to prevent loops back to sender\ndelete msg._socketid;\ndelete msg.socketid;\n\n// Manufacturers query → list for first dropdown\nif (originalTopic === 'getManufacturers') {\n const manufacturers = dbResults\n .map(row => row.manufacturer)\n .filter((mfg, index, arr) => mfg && arr.indexOf(mfg) === index)\n .sort();\n\n msg.topic = 'manufacturersList';\n msg.payload = manufacturers;\n\n node.status({ fill: 'green', shape: 'dot', text: `${manufacturers.length} manufacturers` });\n return msg;\n}\n\n// Preset lists (legacy load/search)\nif (originalTopic === 'loadMoldPresets' || originalTopic === 'searchMoldPresets') {\n const presets = dbResults.map(row => ({\n mold_name: row.mold_name || '',\n manufacturer: row.manufacturer || '',\n theoretical_cavities: Number(row.theoretical_cavities) || 0,\n functional_cavities: Number(row.functional_cavities) || 0\n }));\n\n msg.topic = 'moldPresetsList';\n msg.payload = presets;\n\n node.status({ fill: 'green', shape: 'dot', text: `${presets.length} presets found` });\n return msg;\n}\n\n// Molds for selected manufacturer\nif (originalTopic === 'getMoldsByManufacturer') {\n const presets = dbResults.map(row => ({\n mold_name: row.mold_name || '',\n manufacturer: row.manufacturer || '',\n theoretical_cavities: Number(row.theoretical_cavities) || 0,\n functional_cavities: Number(row.functional_cavities) || 0\n }));\n\n msg.topic = 'moldPresetsList';\n msg.payload = presets;\n\n node.status({ fill: 'blue', shape: 'dot', text: `${presets.length} molds for manufacturer` });\n return msg;\n}\n\n// Result of inserting a new mold\nif (originalTopic === 'addMoldPreset') {\n msg.topic = 'addMoldResult';\n msg.payload = {\n success: true,\n result: msg.payload\n };\n\n node.status({ fill: 'green', shape: 'dot', text: 'Mold added' });\n return msg;\n}\n\nnode.status({ fill: 'yellow', shape: 'ring', text: 'Unknown topic: ' + originalTopic });\nreturn null;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 740, + "y": 600, + "wires": [ + [ + "6b1a37faf14b1a55" + ] + ] + }, + { + "id": "af1c33999012b1c1", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "link out 1", + "mode": "link", + "links": [ + "183adc7ca661960e" + ], + "x": 505, + "y": 480, + "wires": [] + }, + { + "id": "183adc7ca661960e", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "name": "link in 1", + "links": [ + "af1c33999012b1c1" + ], + "x": 215, + "y": 660, + "wires": [ + [ + "746bd5f6d88850e7", + "2c31593fa4f5fbaa", + "f1a89a5092479152", + "c904233ea449e9e0" + ] + ] + }, + { + "id": "1af33da0fece65a7", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Work Order buttons", + "func": "const config = global.get(\"config\") || {};\nconst settings = global.get(\"settings\") || {};\nconst state = global.get(\"state\") || {};\nconst reasonCatalog = settings.reasonCatalog || {};\n\nconst persistMoldCache = () => {\n const cache = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n };\n global.set(\"moldCache\", cache);\n try {\n global.set(\"moldCache\", cache, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst moldByWorkOrder = state.moldByWorkOrder || {};\nconst attachMold = (order) => {\n if (!order || !order.id) return;\n const mapped = moldByWorkOrder[order.id] || {};\n const cavities = Number(order.cavities ?? mapped.active ?? state.lastMoldActive ?? 0);\n if (!Number.isFinite(cavities) || cavities <= 0) return;\n const total = Number(order.cavities_total ?? order.cavitiesTotal ?? mapped.total ?? state.lastMoldTotal ?? settings.moldTotal ?? 0);\n order.cavities = cavities;\n if (Number.isFinite(total) && total > 0) {\n order.cavities_total = total;\n state.lastMoldTotal = total;\n }\n state.lastMoldActive = cavities;\n moldByWorkOrder[order.id] = {\n total: Number.isFinite(total) && total > 0 ? total : (state.lastMoldTotal ?? null),\n active: cavities\n };\n state.moldByWorkOrder = moldByWorkOrder;\n};\n\nconst finalize = (ret) => {\n global.set(\"state\", state);\n persistMoldCache();\n return ret;\n};\n\nconst normalizeReason = (payload, fallbackType) => {\n const path = Array.isArray(payload.reasonPath) ? payload.reasonPath : [];\n const category = path[0] || {};\n const detail = path[1] || {};\n return {\n type: payload.reasonType || fallbackType || \"downtime\",\n categoryId: category.id || null,\n categoryLabel: category.label || null,\n detailId: detail.id || null,\n detailLabel: detail.label || null,\n reasonText: payload.reasonText || [\n category.label || \"\",\n detail.label || \"\"\n ].filter(Boolean).join(\" > \") || null,\n catalogVersion: payload.catalogVersion || reasonCatalog.version || null,\n incidentKey: payload.incidentKey || null\n };\n};\n\n\n// ===== MOLD CHANGE AUTO-CLOSE =====\n// If a mold change is active and user is starting/resuming/restarting a WO, close it.\nif (state.moldChange && state.moldChange.active &&\n (msg.action === \"start\" || msg.action === \"start-work-order\" || msg.action === \"resume-work-order\" || msg.action === \"restart-work-order\")) {\n const now = Date.now();\n const mc = state.moldChange;\n const durationSec = Math.max(0, Math.round((now - (mc.startMs || now)) / 1000));\n const closedMc = {\n active: false,\n startMs: mc.startMs,\n endMs: now,\n fromMoldId: mc.fromMoldId || null,\n toMoldId: state.lastMoldActive || null,\n durationSec\n };\n state.moldChange = closedMc;\n global.set(\"state\", state);\n const closeEvent = {\n payload: {\n anomaly_type: \"mold-change\",\n eventType: \"mold-change\",\n severity: \"info\",\n requires_ack: false,\n title: \"Cambio de molde finalizado (automatico)\",\n description: \"Duracion: \" + Math.round(durationSec/60) + \" min\",\n status: \"resolved\",\n tsMs: now,\n data: {\n status: \"resolved\",\n start_ms: closedMc.startMs,\n end_ms: now,\n duration_sec: durationSec,\n stoppage_duration_seconds: durationSec,\n from_mold_id: closedMc.fromMoldId,\n to_mold_id: closedMc.toMoldId,\n incidentKey: \"mold-change:\" + (closedMc.startMs || now)\n }\n },\n tsMs: now\n };\n const uiClose = { _mode: \"mold-change-status\", payload: { active: false, endMs: now, durationSec } };\n node.send([null, uiClose, null, null, null, closeEvent]);\n}\n\nconst mode = msg._mode || '';\nswitch (msg.action) {\n case \"upload-excel\":\n msg._mode = \"upload\";\n return finalize([msg, null, null, null]);\n case \"refresh-work-orders\": {\n const selectMsg = {\n ...msg,\n _mode: \"select\",\n topic: \"SELECT * FROM work_orders ORDER BY created_at DESC;\"\n };\n const fetchMsg = {\n ...msg,\n topic: \"workOrdersRefresh\",\n forceRefresh: true,\n payload: msg.payload && typeof msg.payload === \"object\" ? msg.payload : {}\n };\n return finalize([null, selectMsg, null, null, null, null, fetchMsg]);\n }\n case \"start-work-order\": {\n msg._mode = \"start-check-progress\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for start\", msg);\n return finalize([null, null, null, null]);\n }\n\n // Store order data temporarily for after DB query\n flow.set(\"pendingWorkOrder\", order);\n\n // Query database to check for existing progress\n msg.topic = \"SELECT cycle_count, good_parts, scrap_parts, progress_percent, target_qty, cavities_total, cavities_active FROM work_orders WHERE work_order_id = ? LIMIT 1\";\n msg.payload = [order.id];\n\n //(`[START-WO] Checking progress for WO ${order.id}`);\n return finalize([null, msg, null, null]);\n }\n case \"resume-work-order\": {\n const now = Date.now();\n state.productionStartTime = now;\n state.actualRunTime = 0;\n state.lastStartChangeTime = now;\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiLastTick = null;\n msg._mode = \"resume\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for resume\", msg);\n return finalize([null, null, null, null]);\n }\n\n //node.warn(`[RESUME-WO] Resuming WO ${order.id} with existing progress`);\n\n // Set status to RUNNING without resetting progress\n msg.topic = \"UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END, cavities_total = COALESCE(NULLIF(?,0), cavities_total), cavities_active = COALESCE(NULLIF(?,0), cavities_active) WHERE status <> 'DONE'\";\n msg.payload = [order.id, order.id, Number(order.cavities_total || order.cavitiesTotal || 0), Number(order.cavities || 0)];\n msg.startOrder = order;\n\n // Load existing values into global state\n // IMPORTANT: Also set scrapParts so goodParts calculation is correct\n order.scrapParts = Number(order.scrapParts) || 0;\n order.goodParts = Number(order.goodParts) || 0;\n\n attachMold(order);\n\n state.activeWorkOrder = order;\n state.cycleCount = Number(order.cycleCount) || 0;\n state.activeOrderHasProgress = true;\n flow.set(\"lastMachineState\", 0);\n state.scrapPromptIssuedFor = null;\n\n //node.warn(`[RESUME-WO] Set cycleCount=${order.cycleCount}, scrapParts=${order.scrapParts}, goodParts=${order.goodParts}`);\n return finalize([null, null, msg, null]);\n }\n case \"restart-work-order\": {\n const now = Date.now();\n state.productionStartTime = now;\n state.actualRunTime = 0;\n state.lastStartChangeTime = now;\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiLastTick = null;\n msg._mode = \"restart\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for restart\", msg);\n return finalize([null, null, null, null]);\n }\n\n //node.warn(`[RESTART-WO] Restarting WO ${order.id} - resetting progress to 0`);\n\n // Reset progress in database AND set status to RUNNING\n msg.topic = \"UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, cycle_count = 0, good_parts = 0, scrap_parts = 0, progress_percent = 0, updated_at = NOW(), cavities_total = COALESCE(NULLIF(?,0), cavities_total), cavities_active = COALESCE(NULLIF(?,0), cavities_active) WHERE work_order_id = ? OR status = 'RUNNING'\";\n msg.payload = [order.id, order.id, Number(order.cavities_total || order.cavitiesTotal || 0), Number(order.cavities || 0), order.id];\n msg.startOrder = order;\n\n // Initialize global state to 0\n order.scrapParts = 0;\n order.goodParts = 0;\n\n attachMold(order);\n\n state.activeWorkOrder = order;\n state.cycleCount = 0;\n state.activeOrderHasProgress = false;\n flow.set(\"lastMachineState\", 0);\n state.scrapPromptIssuedFor = null;\n\n //node.warn(`[RESTART-WO] Reset cycleCount=0, scrap=0, good=0`);\n return finalize([null, null, msg, null]);\n }\n case \"complete-work-order\": {\n state.productionStartTime = null;\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiLastTick = null;\n msg._mode = \"complete\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for complete\", msg);\n return finalize([null, null, null, null]);\n }\n\n // Get final values from global state before clearing\n const activeOrder = state.activeWorkOrder || {};\n const finalCycleCount = Number(state.cycleCount || 0);\n const finalGoodParts = Number(activeOrder.goodParts) || 0;\n const finalScrapParts = Number(activeOrder.scrapParts) || 0;\n\n //node.warn(`[COMPLETE] Persisting final values: cycles=${finalCycleCount}, good=${finalGoodParts}, scrap=${finalScrapParts}`);\n\n msg.completeOrder = order;\n\n // SQL: Persist final counts AND set status to DONE\n msg.topic = \"UPDATE work_orders SET status = 'DONE', cycle_count = ?, good_parts = ?, scrap_parts = ?, progress_percent = 100, updated_at = NOW() WHERE work_order_id = ?\";\n msg.payload = [finalCycleCount, finalGoodParts, finalScrapParts, order.id];\n\n // Clear ALL state on completion\n state.activeWorkOrder = null;\n state.activeOrderHasProgress = false;\n state.trackingEnabled = false;\n state.productionStarted = false;\n state.kpiStartupMode = false;\n state.operatingTime = 0;\n state.lastCycleTime = null;\n state.cycleCount = 0;\n flow.set(\"lastMachineState\", 0);\n state.scrapPromptIssuedFor = null;\n state.actualRunTime = 0;\n state.lastStateChangeTime = null;\n\n if (state.lastState && typeof state.lastState === \"object\") {\n state.lastState = {\n ...state.lastState,\n activeWorkOrder: null,\n cycleCount: 0,\n goodParts: 0,\n scrapParts: 0,\n cycleTime: 0,\n actualCycleTime: 0,\n trackingEnabled: false,\n productionStarted: false,\n tsMs: Date.now()\n };\n }\n\n // ============================================================\n // HIGH SCRAP DETECTION\n // ============================================================\n const targetQty = Number(activeOrder.target) || 0;\n const scrapCount = finalScrapParts;\n const scrapPercent = targetQty > 0 ? (scrapCount / targetQty) * 100 : 0;\n\n // Trigger: Scrap > 10% of target quantity\n let anomalyMsg = null;\n if (scrapPercent > 10 && targetQty > 0) {\n const severity = scrapPercent > 25 ? 'critical' : 'warning';\n\n const highScrapAnomaly = {\n anomaly_type: 'high-scrap',\n severity: severity,\n title: `High Waste Detected`,\n description: `Work order completed with ${scrapCount} scrap parts (${scrapPercent.toFixed(1)}% of target ${targetQty}). Why is there so much waste?`,\n data: {\n scrap_count: scrapCount,\n target_quantity: targetQty,\n scrap_percent: Math.round(scrapPercent * 10) / 10,\n good_parts: finalGoodParts,\n total_cycles: finalCycleCount\n },\n kpi_snapshot: {\n oee: (msg.kpis && msg.kpis.oee) || state.currentKPIs?.oee || 0,\n availability: (msg.kpis && msg.kpis.availability) || state.currentKPIs?.availability || 0,\n performance: (msg.kpis && msg.kpis.performance) || state.currentKPIs?.performance || 0,\n quality: (msg.kpis && msg.kpis.quality) || state.currentKPIs?.quality || 0\n },\n work_order_id: order.id,\n cycle_count: finalCycleCount,\n tsMs: Date.now(),\n status: 'active'\n };\n\n //node.warn(`[HIGH SCRAP] Detected ${scrapPercent.toFixed(1)}% scrap on work order ${order.id}`);\n\n // Send to Event Logger (output 5)\n anomalyMsg = {\n topic: \"anomaly-detected\",\n payload: [highScrapAnomaly]\n };\n }\n\n //node.warn('[COMPLETE] Cleared all state flags');\n return finalize([null, null, null, msg, anomalyMsg]);\n }\n case \"get-current-state\": {\n // Single truth for UI: state.lastState\n const s = state.lastState || {};\n\n const validWorkOrders = state.workOrdersById || null;\n if (state.activeWorkOrder?.id && validWorkOrders && !validWorkOrders[state.activeWorkOrder.id]) {\n node.warn(`[STATE] activeWorkOrder ${state.activeWorkOrder.id} not in work order list; clearing`);\n state.activeWorkOrder = null;\n }\n\n const activeOrder = (state.activeWorkOrder && state.activeWorkOrder.id)\n ? state.activeWorkOrder\n : null;\n\n const trackingEnabled =\n (typeof s.trackingEnabled === \"boolean\" ? s.trackingEnabled : (state.trackingEnabled || false));\n\n const productionStarted =\n (typeof s.productionStarted === \"boolean\" ? s.productionStarted : (state.productionStarted || false));\n\n const kpis =\n (s.kpis && typeof s.kpis === \"object\")\n ? s.kpis\n : (state.currentKPIs || { oee: 0, availability: 0, performance: 0, quality: 0 });\n\n const cavities = Number(activeOrder?.cavities ?? state.lastMoldActive ?? s.cavities ?? 0) || null;\n\n msg._mode = \"current-state\";\n msg.payload = {\n machineId: s.machineId ?? config.machineId ?? undefined,\n\n activeWorkOrder: activeOrder,\n\n // add the fields your UI / home tab might show\n cycleCount: s.cycleCount,\n goodParts: s.goodParts,\n scrapParts: s.scrapParts,\n cavities,\n\n cycleTime: s.cycleTime,\n actualCycleTime: s.actualCycleTime,\n\n trackingEnabled,\n productionStarted,\n kpis,\n reasonCatalog,\n\n tsMs: s.tsMs\n };\n\n return finalize([null, msg, null, null]);\n }\n case \"restore-session\": {\n // Query DB for any RUNNING work order on startup\n msg._mode = \"restore-query\";\n msg.topic = \"SELECT * FROM work_orders WHERE status = 'RUNNING' LIMIT 1\";\n msg.payload = [];\n //node.warn('[RESTORE] Checking for running work order on startup');\n return finalize([null, msg, null, null]);\n }\n case \"scrap-entry\": {\n const { id, scrap } = msg.payload || {};\n const scrapNum = Number(scrap) || 0;\n\n if (!id) {\n node.error(\"No work order id supplied for scrap entry\", msg);\n return finalize([null, null, null, null]);\n }\n\n const activeOrder = state.activeWorkOrder;\n if (activeOrder && activeOrder.id === id) {\n activeOrder.scrapParts = (Number(activeOrder.scrapParts) || 0) + scrapNum;\n state.activeWorkOrder = activeOrder;\n }\n\n state.scrapPromptIssuedFor = null;\n\n msg._mode = \"scrap-update\";\n msg.scrapEntry = { id, scrap: scrapNum, reason: null };\n // SQL with bound parameters for safety\n msg.topic = \"UPDATE work_orders SET scrap_parts = scrap_parts + ?, updated_at = NOW() WHERE work_order_id = ?\";\n msg.payload = [scrapNum, id];\n\n return finalize([null, null, msg, null]);\n }\n case \"scrap-entry-with-reason\": {\n const { id, scrap } = msg.payload || {};\n const scrapNum = Number(scrap) || 0;\n const reason = normalizeReason(msg.payload || {}, \"scrap\");\n\n if (!id) {\n node.error(\"No work order id supplied for scrap entry with reason\", msg);\n return finalize([null, null, null, null, null, null]);\n }\n\n const activeOrder = state.activeWorkOrder;\n if (activeOrder && activeOrder.id === id) {\n activeOrder.scrapParts = (Number(activeOrder.scrapParts) || 0) + scrapNum;\n state.activeWorkOrder = activeOrder;\n }\n\n state.scrapPromptIssuedFor = null;\n state.lastScrapReasonByOrder = state.lastScrapReasonByOrder || {};\n state.lastScrapReasonByOrder[id] = reason;\n\n const tsMs = Date.now();\n const outEvent = {\n tsMs,\n eventType: \"scrap-manual-entry\",\n workOrderId: id,\n scrapDelta: scrapNum,\n source: \"home-ui\",\n // Keep compatibility for current event ingest.\n reason,\n // Explicit split marker for future downtime-specific stream.\n downtime: null\n };\n\n msg._mode = \"scrap-update\";\n msg.scrapEntry = { id, scrap: scrapNum, reason };\n msg.topic = \"UPDATE work_orders SET scrap_parts = scrap_parts + ?, updated_at = NOW() WHERE work_order_id = ?\";\n msg.payload = [scrapNum, id];\n\n // Output 3 -> DB update path, Output 6 -> event outbox builder path.\n return finalize([null, null, msg, null, null, { payload: outEvent, tsMs }]);\n }\n case \"scrap-open\": {\n const active = state.activeWorkOrder || null;\n if (!active?.id) return finalize([null, null, null, null, null]);\n\n const good = Number(active.goodParts) || 0;\n const scrap = Number(active.scrapParts) || 0;\n const produced = good + scrap;\n\n msg._mode = \"scrap-prompt\";\n msg.scrapPrompt = {\n id: active.id,\n sku: active.sku || \"\",\n target: Number(active.target) || 0,\n produced,\n scrapSoFar: scrap,\n\n manual: true,\n title: \"Registrar scrap (parcial)\",\n enterMode: true,\n validateMax: false,\n reasonCatalog: reasonCatalog\n };\n\n // IMPORTANT: don't set msg.topic to a string here\n delete msg.topic;\n delete msg.payload;\n\n // Output 2 feeds Back to UI in your wiring\n return finalize([null, msg, null, null, null]);\n }\n\n\n case \"scrap-skip\": {\n const { id, remindAgain } = msg.payload || {};\n\n if (!id) {\n node.error(\"No work order id supplied for scrap skip\", msg);\n return finalize([null, null, null, null]);\n }\n\n if (remindAgain) {\n state.scrapPromptIssuedFor = null;\n }\n\n msg._mode = \"scrap-skipped\";\n return finalize([null, null, null, null]);\n }\n case \"start\": {\n // START with KPI timestamp init - FIXED\n const now = Date.now();\n const shifts = settings.shifts || [{ start: '06:00', end: '13:00' }];\n const shiftChangeComp = settings.shiftChangeCompensation || 10; // minutes\n const lunchBreak = settings.lunchBreakMinutes || 30; // minutes\n\n\n let totalShiftSeconds = 0;\n shifts.forEach(shift => {\n const [startH, startM] = (shift.start || '06:00').split(':').map(Number);\n const [endH, endM] = (shift.end || '15:00').split(':').map(Number);\n\n let startMinutes = startH * 60 + startM;\n let endMinutes = endH * 60 + endM;\n\n // Handle overnight shifts\n if (endMinutes <= startMinutes) {\n endMinutes += 24 * 60;\n }\n\n totalShiftSeconds += (endMinutes - startMinutes) * 60;\n });\n const compensationSeconds = shifts.length * shiftChangeComp * 60; // shift change per shift\n const lunchSeconds = lunchBreak * 60;\n const plannedProductionTime = Math.max(0, totalShiftSeconds - compensationSeconds - lunchSeconds);\n state.plannedProductionTime = plannedProductionTime;\n\n const existingCycles = Number(state.cycleCount || 0);\n const activeOrderState = state.activeWorkOrder || {};\n const orderGood = Number(activeOrderState.goodParts) || 0;\n const orderScrap = Number(activeOrderState.scrapParts) || 0;\n\n const hasProgressFlag = state.activeOrderHasProgress;\n\n // We consider there is progress only if this order has produced something\n const hasProgress = (typeof hasProgressFlag === 'boolean')\n ? hasProgressFlag\n : (\n existingCycles > 0 ||\n (orderGood + orderScrap) > 0\n );\n\n if (!hasProgress) {\n state.stopTime = 0; // Initialize stop time\n state.trackingEnabled = true;\n state.trackingEnabled = true;\n state.productionStarted = true;\n state.kpiStartupMode = true;\n state.kpiBuffer = [];\n state.lastKPIRecordTime = now - 60000;\n state.productionStartTime = now;\n state.lastMachineCycleTime = now;\n state.lastCycleTime = now;\n state.operatingTime = 0;\n state.actualRunTime = 0;\n state.lastStartChangeTime = now;\n state.lastCycleCompletionTime = null; // Will be set on first cycle\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiLastTick = null;\n } else {\n state.trackingEnabled = true;\n state.productionStarted = true;\n // Do NOT go back into startup mode\n state.kpiStartupMode = false;\n }\n\n //node.warn('[START] Initialized: trackingEnabled=true, productionStarted=true, kpiStartupMode=true, operatingTime=0');\n //node.warn(`[START] Planned production time: ${(plannedProductionTime / 3600).toFixed(2)} hours`);\n\n const activeOrder = state.activeWorkOrder || {};\n msg._mode = \"production-state\";\n\n msg.payload = msg.payload || {};\n\n msg.trackingEnabled = true;\n msg.productionStarted = true;\n msg.machineOnline = true;\n\n msg.payload.trackingEnabled = true;\n msg.payload.productionStarted = true;\n msg.payload.machineOnline = true;\n\n return finalize([null, msg, null, null]);\n }\n case \"stop\": {\n state.trackingEnabled = false;\n state.productionStarted = false;\n //node.warn('[STOP] Set trackingEnabled=false, productionStarted=false');\n\n // Send UI update so button state reflects change\n msg._mode = \"production-state\";\n msg.payload = msg.payload || {};\n msg.trackingEnabled = false;\n msg.productionStarted = false;\n msg.machineOnline = true;\n msg.payload.trackingEnabled = false;\n msg.payload.productionStarted = false;\n msg.payload.machineOnline = true;\n\n const s = state.lastState || {};\n s.trackingEnabled = false;\n s.productionStarted = false;\n s.tsMs = Date.now();\n state.lastState = s;\n\n return finalize([null, msg, null, null]);\n }\n case \"start-tracking\": {\n const activeOrder = state.activeWorkOrder || {};\n\n if (!activeOrder.id) {\n return finalize([null, { topic: \"alert\", payload: \"Error: No active work order loaded.\" }, null, null]);\n }\n\n const now = Date.now();\n state.trackingEnabled = true;\n\n // reset KPI timers/counters (keep what you had)\n state.kpiBuffer = [];\n state.lastKPIRecordTime = now - 60000;\n state.lastMachineCycleTime = now;\n state.lastCycleTime = now;\n state.operatingTime = 0.001;\n\n // IMPORTANT: do NOT set productionStarted here (that’s machine physical state)\n const stateMsg = {\n _mode: \"production-state\",\n payload: {\n trackingEnabled: true,\n machineOnline: true\n }\n };\n\n return finalize([null, stateMsg, null, null]);\n }\ncase \"start-mold-change\": {\n const now = Date.now();\n const p = msg.payload || {};\n const mc = {\n active: true,\n startMs: now,\n endMs: null,\n fromMoldId: p.fromMoldId || state.lastMoldActive || null,\n toMoldId: null,\n operator: p.operator || null\n };\n state.moldChange = mc;\n // Disable tracking during mold change so KPI/anomaly are paused\n state.trackingEnabled = false;\n state.productionStarted = false;\n const event = {\n payload: {\n anomaly_type: \"mold-change\",\n eventType: \"mold-change\",\n severity: \"info\",\n requires_ack: false,\n title: \"Cambio de molde iniciado\",\n description: \"Inicio de cambio de molde\",\n status: \"active\",\n tsMs: now,\n data: {\n status: \"active\",\n start_ms: now,\n from_mold_id: mc.fromMoldId,\n operator: mc.operator,\n incidentKey: \"mold-change:\" + now\n }\n },\n tsMs: now\n };\n const uiMsg = { _mode: \"mold-change-status\", payload: { active: true, startMs: now, fromMoldId: mc.fromMoldId } };\n return finalize([null, uiMsg, null, null, null, event]);\n }\n case \"end-mold-change\": {\n const now = Date.now();\n const p = msg.payload || {};\n const mc = state.moldChange || {};\n if (!mc.active) {\n return finalize([null, null, null, null, null, null]);\n }\n const durationSec = Math.max(0, Math.round((now - (mc.startMs || now)) / 1000));\n const closed = {\n active: false,\n startMs: mc.startMs,\n endMs: now,\n fromMoldId: mc.fromMoldId || null,\n toMoldId: p.toMoldId || state.lastMoldActive || null,\n durationSec\n };\n state.moldChange = closed;\n const event = {\n payload: {\n anomaly_type: \"mold-change\",\n eventType: \"mold-change\",\n severity: \"info\",\n requires_ack: false,\n title: \"Cambio de molde finalizado\",\n description: \"Duracion: \" + Math.round(durationSec/60) + \" min\",\n status: \"resolved\",\n tsMs: now,\n data: {\n status: \"resolved\",\n start_ms: closed.startMs,\n end_ms: now,\n duration_sec: durationSec,\n stoppage_duration_seconds: durationSec,\n from_mold_id: closed.fromMoldId,\n to_mold_id: closed.toMoldId,\n incidentKey: \"mold-change:\" + (closed.startMs || now)\n }\n },\n tsMs: now\n };\n const uiMsg = { _mode: \"mold-change-status\", payload: { active: false, endMs: now, durationSec } };\n return finalize([null, uiMsg, null, null, null, event]);\n }\n\n}\n", + "outputs": 7, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1150, + "y": 680, + "wires": [ + [ + "07b66a930e3e5e40" + ], + [ + "ddf6d786b5a7e682", + "2872629594daf0ed", + "af0fb4f5a75407ac", + "8c52b16f9c4671d0", + "5c8843053781fbd9" + ], + [ + "037e4825e2537ed2", + "ddf6d786b5a7e682", + "8c52b16f9c4671d0", + "17b04a44024da6f2" + ], + [ + "037e4825e2537ed2" + ], + [ + "4a382caf6dadab4c" + ], + [ + "a1e59ef54a91ee50" + ], + [ + "8abddc40064f1f64" + ] + ] + }, + { + "id": "c7a8bb7adab6838e", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "link out 2", + "mode": "link", + "links": [ + "fc87eda3d68142da" + ], + "x": 585, + "y": 300, + "wires": [] + }, + { + "id": "fc87eda3d68142da", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link in 2", + "links": [ + "c7a8bb7adab6838e" + ], + "x": 1305, + "y": 540, + "wires": [ + [ + "1af33da0fece65a7" + ] + ] + }, + { + "id": "a0a8bda316e17f8e", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Build Insert SQL", + "func": "const rows = Array.isArray(msg.payload) ? msg.payload : [];\n\nconst values = rows\n .map((r) => {\n const id = String(r[\"Work Order ID\"] ?? \"\").trim();\n if (!id) return null;\n\n const sku = String(r[\"SKU\"] ?? \"\").trim();\n const targetQty = Number(r[\"Target Quantity\"]) || 0;\n const cycleTime =\n Number(r[\"Theoretical Cycle Time (Seconds)\"]) || 0;\n\n return [id, sku, targetQty, cycleTime, \"PENDING\"];\n })\n .filter(Boolean);\n\nif (!values.length) {\n return null;\n}\n\nmsg.topic = `\n INSERT INTO work_orders\n (work_order_id, sku, target_qty, cycle_time, status)\n VALUES ?\n ON DUPLICATE KEY UPDATE\n sku = VALUES(sku),\n target_qty = VALUES(target_qty),\n cycle_time = VALUES(cycle_time);\n`;\n\nmsg.payload = [values];\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1800, + "y": 500, + "wires": [ + [ + "037e4825e2537ed2" + ] + ] + }, + { + "id": "037e4825e2537ed2", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "mydb": "fc9634aabefee16b", + "name": "mariaDB", + "x": 1820, + "y": 920, + "wires": [ + [ + "4bb96d61f9e3bba2" + ] + ] + }, + { + "id": "2848a91e7a88cd0b", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "g": "8b73333804b30ee0", + "mydb": "fc9634aabefee16b", + "name": "mariaDB (Graph Data)", + "x": 1600, + "y": 80, + "wires": [ + [ + "89d2d406e3cd96ee" + ] + ] + }, + { + "id": "2872629594daf0ed", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Back to UI", + "func": "const mode = msg._mode || '';\nconst started = msg.startOrder || null;\nconst completed = msg.completeOrder || null;\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\n\nconst loadMoldCache = () => {\n let cache = null;\n try {\n cache = global.get(\"moldCache\", \"file\");\n } catch (err) {\n cache = null;\n }\n if (!cache || typeof cache !== \"object\") {\n cache = global.get(\"moldCache\") || {};\n }\n return cache;\n};\n\nconst moldCache = loadMoldCache();\nconst cachedByWorkOrder = (moldCache && typeof moldCache.moldByWorkOrder === \"object\")\n ? moldCache.moldByWorkOrder\n : {};\n\nstate.moldByWorkOrder = state.moldByWorkOrder || cachedByWorkOrder || {};\nif (!Number.isFinite(state.lastMoldActive) && Number.isFinite(moldCache.lastMoldActive)) {\n state.lastMoldActive = moldCache.lastMoldActive;\n}\nif (!Number.isFinite(state.lastMoldTotal) && Number.isFinite(moldCache.lastMoldTotal)) {\n state.lastMoldTotal = moldCache.lastMoldTotal;\n}\n\nconst persistMoldCache = () => {\n const cache = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n };\n global.set(\"moldCache\", cache);\n try {\n global.set(\"moldCache\", cache, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst attachMold = (order) => {\n if (!order || !order.id) return;\n const map = state.moldByWorkOrder || {};\n const mapped = map[order.id] || {};\n const resolved = Number(\n order.cavities ?? mapped.active ?? state.lastMoldActive ?? settings.moldActive ?? 0\n );\n if (!Number.isFinite(resolved) || resolved <= 0) return;\n order.cavities = resolved;\n state.lastMoldActive = resolved;\n map[order.id] = {\n total: Number.isFinite(mapped.total) && mapped.total > 0 ? mapped.total : (state.lastMoldTotal ?? null),\n active: resolved\n };\n state.moldByWorkOrder = map;\n};\n\nconst finalize = (ret) => {\n global.set(\"state\", state);\n persistMoldCache();\n return ret;\n};\nconst toIsoString = (v) => {\n if (!v) return null;\n if (typeof v === \"string\") return v;\n if (v instanceof Date) return v.toISOString();\n if (typeof v.toISOString === \"function\") return v.toISOString(); // date-like\n if (typeof v.toISO === \"function\") return v.toISO(); // luxon\n if (typeof v.format === \"function\") return v.format(); // moment\n return String(v);\n};\n\ndelete msg._mode;\ndelete msg.startOrder;\ndelete msg.completeOrder;\ndelete msg.action;\ndelete msg.filename;\n\n//node.warn(`[ROUTER] mode=\"${mode}\" action=\"${msg.action}\"`);\n\n// ========================================================\n// MODE: UPLOAD\n// ========================================================\nif (mode === \"upload\") {\n msg.topic = \"uploadStatus\";\n msg.payload = { message: \"✅ Work orders uploaded successfully.\" };\n return finalize([msg, null, null, null]);\n}\n\n// ========================================================\n// MODE: SELECT (Load Work Orders)\n// ========================================================\nif (mode === \"select\") {\n const rawRows = Array.isArray(msg.payload) ? msg.payload : [];\n msg.topic = \"workOrdersList\";\n msg.payload = rawRows.map(row => ({\n id: row.work_order_id ?? row.id ?? \"\",\n sku: row.sku ?? \"\",\n target: Number(row.target_qty ?? row.target ?? 0),\n goodParts: Number(row.good_parts ?? row.goodParts ?? 0),\n scrapParts: Number(row.scrap_count ?? row.scrap_parts ?? row.scrapParts ?? 0),\n progressPercent: Number(row.progress_percent ?? row.progress ?? 0),\n status: (row.status ?? \"PENDING\").toUpperCase(),\n lastUpdateIso: toIsoString(row.updated_at ?? row.last_update),\n cycleTime: Number(row.cycle_time ?? row.theoretical_cycle_time ?? 0)\n }));\n return finalize([msg, null, null, null]);\n}\n\n// ========================================================\n// MODE: START WORK ORDER\n// ========================================================\nif (mode === \"start\") {\n const order = started || {};\n attachMold(order);\n const kpis = msg.kpis || state.currentKPIs || {\n oee: 0, availability: 0, performance: 0, quality: 0\n };\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: {\n id: order.id || \"\",\n sku: order.sku || \"\",\n target: Number(order.target) || 0,\n goodParts: Number(order.goodParts) || 0,\n scrapParts: Number(order.scrapParts) || 0,\n cycleTime: Number(order.cycleTime || order.theoreticalCycleTime || 0),\n progressPercent: Number(order.progressPercent) || 0,\n lastUpdateIso: toIsoString(order.lastUpdateIso) || null,\n kpis: kpis\n }\n };\n\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: COMPLETE WORK ORDER\n// ========================================================\nif (mode === \"complete\") {\n const homeMsg = { topic: \"activeWorkOrder\", payload: null };\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: CYCLE UPDATE DURING PRODUCTION\n// ========================================================\nif (mode === \"cycle\") {\n const cycle = msg.cycle || {};\n\n const workOrderMsg = {\n topic: \"workOrderCycle\",\n payload: {\n id: cycle.id || \"\",\n sku: cycle.sku || \"\",\n target: Number(cycle.target) || 0,\n goodParts: Number(cycle.goodParts) || 0,\n scrapParts: Number(cycle.scrapParts) || 0,\n progressPercent: Number(cycle.progressPercent) || 0,\n lastUpdateIso: toIsoString(cycle.lastUpdateIso) || null,\n status: cycle.progressPercent >= 100 ? \"DONE\" : \"RUNNING\"\n }\n };\n\n const kpis = msg.kpis || state.currentKPIs || {\n oee: 0, availability: 0, performance: 0, quality: 0\n };\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: {\n id: cycle.id || \"\",\n sku: cycle.sku || \"\",\n target: Number(cycle.target) || 0,\n goodParts: Number(cycle.goodParts) || 0,\n scrapParts: Number(cycle.scrapParts) || 0,\n cycleTime: Number(cycle.cycleTime) || 0,\n progressPercent: Number(cycle.progressPercent) || 0,\n lastUpdateIso: toIsoString(cycle.lastUpdateIso) || new Date().toISOString(),\n kpis: kpis\n }\n };\n\n return finalize([workOrderMsg, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: MACHINE PRODUCTION STATE\n// ========================================================\nif (mode === \"production-state\") {\n const homeMsg = {\n topic: \"machineStatus\",\n payload: {\n machineOnline: msg.machineOnline ?? true,\n productionStarted: !!msg.productionStarted,\n trackingEnabled: msg.payload?.trackingEnabled ?? msg.trackingEnabled ?? false\n }\n };\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: CURRENT STATE (for tab switch sync)\n// ========================================================\nif (mode === \"current-state\") {\n const state = msg.payload || {};\n const homeMsg = {\n topic: \"currentState\",\n payload: {\n activeWorkOrder: state.activeWorkOrder,\n trackingEnabled: state.trackingEnabled,\n productionStarted: state.productionStarted,\n kpis: state.kpis\n }\n };\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: RESTORE QUERY (startup state recovery)\n// ========================================================\nif (mode === \"restore-query\") {\n const rows = Array.isArray(msg.payload) ? msg.payload : [];\n\n if (rows.length > 0) {\n const row = rows[0];\n const restoredOrder = {\n id: row.work_order_id || row.id || \"\",\n sku: row.sku || \"\",\n target: Number(row.target_qty || row.target || 0),\n goodParts: Number(row.good_parts || row.goodParts || 0),\n scrapParts: Number(row.scrap_parts || row.scrapParts || 0),\n progressPercent: Number(row.progress_percent || 0),\n cycleTime: Number(row.cycle_time || 0),\n lastUpdateIso: toIsoString(row.updated_at ?? row.last_update),\n cavities: Number(row.cavities_active || 0),\n cavities_total: Number(row.cavities_total || 0)\n };\n\n attachMold(restoredOrder);\n\n if (Number(restoredOrder.cavities) > 0) {\n state.lastMoldActive = Number(restoredOrder.cavities);\n }\n if (Number(restoredOrder.cavities_total) > 0) {\n state.lastMoldTotal = Number(restoredOrder.cavities_total);\n }\n if (restoredOrder.id && Number(restoredOrder.cavities) > 0) {\n const map = state.moldByWorkOrder || {};\n map[restoredOrder.id] = {\n total: Number(restoredOrder.cavities_total) > 0 ? Number(restoredOrder.cavities_total) : (state.lastMoldTotal ?? null),\n active: Number(restoredOrder.cavities)\n };\n state.moldByWorkOrder = map;\n }\n\n const restoredCycleCount = Number(row.cycle_count) || 0;\n const restoredGood = Number(row.good_parts || 0);\n const restoredScrap = Number(row.scrap_parts || 0);\n const hasProgress = restoredCycleCount > 0 || (restoredGood + restoredScrap) > 0;\n\n // Restore global state\n state.activeWorkOrder = restoredOrder;\n state.cycleCount = restoredCycleCount;\n // Auto-resume tracking for RUNNING work order\n state.trackingEnabled = true;\n state.productionStarted = true;\n state.kpiStartupMode = !hasProgress;\n\n // Reset tick so KPI integration doesn't jump\n state.kpiLastTick = null;\n\n let restoreTs = Date.parse(row.updated_at || \"\");\n if (!isFinite(restoreTs)) {\n restoreTs = Date.now();\n }\n state.lastMachineCycleTime = restoreTs;\n state.lastCycleCompletionTime = restoreTs;\n\n node.warn('[RESTORE] Restored work order: ' + restoredOrder.id + ' with ' + state.cycleCount + ' cycles');\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: restoredOrder\n };\n\n const stateMsg = {\n topic: \"machineStatus\",\n payload: {\n machineOnline: true,\n productionStarted: true,\n trackingEnabled: true\n }\n };\n\n // Set status back to RUNNING in database (if not already DONE)\n // This prevents user from having to \"Load\" the work order again\n const dbMsg = {\n topic: \"UPDATE work_orders SET status = 'RUNNING', updated_at = NOW() WHERE work_order_id = ? AND status != 'DONE'\",\n payload: [restoredOrder.id]\n };\n\n\n return finalize([dbMsg, [homeMsg, stateMsg], null, null]);\n } else {\n node.warn('[RESTORE] No running work order found');\n }\n return finalize([null, null, null, null]);\n}\n\n// ========================================================\n// MODE: SCRAP PROMPT\n// ========================================================\nif (mode === \"scrap-prompt\") {\n const prompt = msg.scrapPrompt || {};\n\n const homeMsg = { topic: \"scrapPrompt\", payload: prompt };\n const tabMsg = { ui_control: { tab: \"Home\" } };\n\n // output1: nothing\n // output2: home template\n // output3: tab navigation\n // output4: graphs template (unused here)\n return finalize([null, homeMsg, tabMsg, null]);\n}\n\n// ========================================================\n// MODE: SCRAP UPDATE\n// ========================================================\nif (mode === \"scrap-update\") {\n const activeOrder = state.activeWorkOrder || {};\n const kpis = msg.kpis || state.currentKPIs || {\n oee: 0, availability: 0, performance: 0, quality: 0\n };\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: {\n id: activeOrder.id || \"\",\n sku: activeOrder.sku || \"\",\n target: Number(activeOrder.target) || 0,\n goodParts: Number(activeOrder.goodParts) || 0,\n scrapParts: Number(activeOrder.scrapParts) || 0,\n cycleTime: Number(activeOrder.cycleTime) || 0,\n progressPercent: Number(activeOrder.progressPercent) || 0,\n lastUpdateIso: toIsoString(activeOrder.lastUpdateIso) || new Date().toISOString(),\n kpis: kpis\n }\n };\n\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: SCRAP COMPLETE\n// ========================================================\nif (mode === \"scrap-complete\") {\n const homeMsg = { topic: \"activeWorkOrder\", payload: null };\n return finalize([null, homeMsg, null, null]);\n}\n\n// ========================================================\n// MODE: CHARTS → SEND REAL DATA TO GRAPH TEMPLATE\n// ========================================================\n//if (mode === \"charts\") {\n\n// const realOEE = msg.realOEE || state.realOEE || [];\n// const realAvailability = msg.realAvailability || state.realAvailability || [];\n// const realPerformance = msg.realPerformance || state.realPerformance || [];\n// const realQuality = msg.realQuality || state.realQuality || [];\n\n// const chartsMsg = {\n// topic: \"chartsData\",\n// payload: {\n// oee: realOEE,\n// availability: realAvailability,\n// performance: realPerformance,\n// quality: realQuality\n// }\n// };\n\n// Send ONLY to output #4\n// return [null, null, null, chartsMsg];\n//}\n\n\n// ========================================================\n// MODE: RESUME-PROMPT\n// ========================================================\nif (mode === \"resume-prompt\") {\n const order = msg.payload.order || null;\n\n if (order) {\n attachMold(order);\n state.activeWorkOrder = order;\n node.warn(\n `[RESUME-PROMPT] activeWorkOrder set to ${order.id}`\n );\n }\n\n const homeMsg = {\n topic: msg.topic || \"resumePrompt\",\n payload: msg.payload,\n };\n\n const kpis =\n msg.kpis ||\n state.currentKPIs || {\n oee: 0,\n availability: 0,\n performance: 0,\n quality: 0,\n };\n\n const activeMsg = order\n ? {\n topic: \"activeWorkOrder\",\n payload: {\n id: order.id || \"\",\n sku: order.sku || \"\",\n target: Number(order.target) || 0,\n goodParts: Number(order.goodParts) || 0,\n scrapParts: Number(order.scrapParts) || 0,\n cycleTime: Number(\n order.cycleTime ||\n order.theoreticalCycleTime ||\n 0\n ),\n progressPercent: Number(\n msg.payload?.progressPercent ??\n order.progressPercent ??\n 0\n ),\n lastUpdateIso: order.lastUpdateIso || null,\n kpis,\n },\n }\n : null;\n\n return finalize([\n null,\n activeMsg ? [activeMsg, homeMsg] : homeMsg,\n null,\n null,\n ]);\n}\n\n\n// ========================================================\n\n// ========================================================\n// MODE: MOLD CHANGE STATUS\n// ========================================================\nif (mode === \"mold-change-status\") {\n const homeMsg = {\n topic: \"moldChangeStatus\",\n payload: msg.payload || {}\n };\n return finalize([null, homeMsg, null, null]);\n}\n\n// DEFAULT\n// ========================================================\nreturn finalize([null, null, null, null]);", + "outputs": 4, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2190, + "y": 560, + "wires": [ + [ + "0fe0cdc07941b31e" + ], + [ + "21e9ba4f889da77f" + ], + [ + "cb972412abfe909f" + ], + [] + ] + }, + { + "id": "0fe0cdc07941b31e", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link out 3", + "mode": "link", + "links": [ + "8f16fdca6d059017" + ], + "x": 2305, + "y": 520, + "wires": [] + }, + { + "id": "8f16fdca6d059017", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "link in 3", + "links": [ + "0fe0cdc07941b31e" + ], + "x": 275, + "y": 320, + "wires": [ + [ + "405dccd3e09a99d4" + ] + ] + }, + { + "id": "5df1ee6ee335a44a", + "type": "book", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "", + "raw": false, + "x": 1810, + "y": 460, + "wires": [ + [ + "a75b33fb4e9ebdf8" + ] + ] + }, + { + "id": "a75b33fb4e9ebdf8", + "type": "sheet", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "", + "sheetName": "Sheet1", + "x": 1930, + "y": 460, + "wires": [ + [ + "7cb1c8d40f510e0b" + ] + ] + }, + { + "id": "7cb1c8d40f510e0b", + "type": "sheet-to-json", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "", + "raw": "false", + "range": "", + "header": "default", + "blankrows": false, + "x": 2070, + "y": 460, + "wires": [ + [ + "a0a8bda316e17f8e" + ] + ] + }, + { + "id": "07b66a930e3e5e40", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Base64", + "func": "const filename =\n msg.filename ||\n (msg.meta && msg.meta.filename) ||\n (msg.payload && msg.payload.filename) ||\n msg.name ||\n 'upload.xlsx';\n\nconst candidates = [];\nif (typeof msg.payload === 'string') candidates.push(msg.payload);\nif (msg.payload && typeof msg.payload.payload === 'string') candidates.push(msg.payload.payload);\nif (msg.payload && typeof msg.payload.file === 'string') candidates.push(msg.payload.file);\nif (msg.payload && typeof msg.payload.base64 === 'string') candidates.push(msg.payload.base64);\nif (typeof msg.file === 'string') candidates.push(msg.file);\nif (typeof msg.data === 'string') candidates.push(msg.data);\n\nfunction stripDataUrl(s) {\n return (s && s.startsWith('data:')) ? s.split(',')[1] : s;\n}\n\nlet b64 = candidates.map(stripDataUrl).find(s => typeof s === 'string' && s.length > 0);\nif (!b64 && Buffer.isBuffer(msg.payload)) { msg.filename = filename; return msg; }\nif (!b64) { node.error('No base64 data found on msg', msg); return null; }\n\nmsg.payload = Buffer.from(b64, 'base64');\nmsg.filename = filename;\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1680, + "y": 460, + "wires": [ + [ + "5df1ee6ee335a44a" + ] + ] + }, + { + "id": "21e9ba4f889da77f", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link out 4", + "mode": "link", + "links": [ + "47a6ea4de26fadce" + ], + "x": 2305, + "y": 560, + "wires": [] + }, + { + "id": "47a6ea4de26fadce", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "link in 4", + "links": [ + "21e9ba4f889da77f" + ], + "x": 275, + "y": 280, + "wires": [ + [ + "87dc48bbe753ab8d" + ] + ] + }, + { + "id": "97399876ca829d1a", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Refresh Trigger", + "func": "if (msg._mode === \"sync-work-orders\") {\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, null];\n}\nif (msg._mode === \"start\" || msg._mode === \"complete\" || msg._mode === \"resume\" || msg._mode === \"restart\") {\n // Preserve original message for Back to UI (output 2)\n const originalMsg = {...msg};\n // Create select message for refreshing WO table (output 1)\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, originalMsg];\n}\nif (msg._mode === \"cycle\" || msg._mode === \"production-state\") {\n return [null, msg];\n}\nif (msg._mode === \"scrap-prompt\") {\n return [null, msg];\n}\nif (msg._mode === \"restore-query\") {\n // Pass restore query results to Back to UI\n return [null, msg];\n}\nif (msg._mode === \"current-state\") {\n // Pass current state to Back to UI\n return [null, msg];\n}\nif (msg._mode === \"scrap-complete\") {\n // Preserve original message for Back to UI (output 2)\n const originalMsg = {...msg};\n // Create select message for refreshing WO table (output 1)\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, originalMsg];\n}\nreturn [null, msg];", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1960, + "y": 600, + "wires": [ + [ + "037e4825e2537ed2" + ], + [ + "2872629594daf0ed" + ] + ] + }, + { + "id": "ac25819b6b50ab90", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "c95c05b78d8464c5", + "name": "Machine cycles", + "func": "const current = Number(msg.payload) || 0;\nconst now = Date.now();\n\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\nconst saveState = () => global.set(\"state\", state);\nconst finalize = (ret) => { saveState(); return ret; };\n\nlet zeroStreak = flow.get(\"zeroStreak\") || 0;\nzeroStreak = current === 0 ? zeroStreak + 1 : 0;\nflow.set(\"zeroStreak\", zeroStreak);\n\nconst prev = flow.get(\"lastMachineState\") ?? 0;\n\n// =============================================\n// TRACK ACTUAL RUN TIME (must happen BEFORE any early returns)\n// Only track if we have an active work order and tracking is enabled\n// =============================================\nconst activeOrder = state.activeWorkOrder;\nconst trackingEnabled = !!state.trackingEnabled;\n\n//if (trackingEnabled && activeOrder && activeOrder.id) {\n // =============================================\n // THRESHOLD-BASED STOPPAGE DETECTION\n // Runs on EVERY state change to detect gaps\n // =============================================\n /*\n const lastCycleTime = state.lastCycleCompletionTime || now;\n const timeSinceLastCycle = now - lastCycleTime;\n const deltaSeconds = timeSinceLastCycle / 1000;\n \n const thresholdMultiplier = settings.thresholdMultiplier || 1.5;\n const Tideal = Number(activeOrder.cycleTime) || 5; // seconds\n const ThresholdParo = Tideal * thresholdMultiplier;\n \n // Only analyze gaps when we have a previous cycle to compare against\n // and when transitioning TO state=1 (cycle completion)\n if (current === 1 && prev === 0 && state.lastCycleCompletionTime) {\n let operatingTime = state.operatingTime || 0;\n let stopTime = state.stopTime || 0;\n \n if (deltaSeconds <= ThresholdParo) {\n // Normal operation - all time counts as running\n operatingTime += deltaSeconds;\n } else {\n // Gap detected - split into running + stopped\n operatingTime += Tideal; // Credit one ideal cycle worth (machine was running for that)\n stopTime += (deltaSeconds - Tideal); // Rest is unplanned downtime\n node.warn(`[STOPPAGE] Detected ${(deltaSeconds - Tideal).toFixed(1)}s downtime (gap: ${deltaSeconds.toFixed(1)}s, threshold: ${ThresholdParo.toFixed(1)}s)`);\n }\n \n state.operatingTime = operatingTime;\n state.stopTime = stopTime;\n }\n \n // =============================================\n // LEGACY: State-based run time tracking (keep as backup/comparison)\n // =============================================\n if (prev === 1) {\n const lastStateChange = state.lastStateChangeTime || now;\n const runDuration = (now - lastStateChange) / 1000;\n\n if (runDuration > 0 && runDuration < 3600) {\n let actualRunTime = state.actualRunTime || 0;\n actualRunTime += runDuration;\n state.actualRunTime = actualRunTime;\n }\n }\n */\n//}\n\n// ALWAYS update state tracking (before any early returns)\nstate.lastStateChangeTime = now;\nflow.set(\"lastMachineState\", current);\n\n// =============================================\n// MACHINE ONLINE STATUS\n// =============================================\nstate.machineOnline = true;\n\nlet productionRunning = !!state.productionStarted;\nlet stateChanged = false;\n\nif (current === 1 && !productionRunning) {\n productionRunning = true;\n stateChanged = true;\n} else if (current === 0 && zeroStreak >= 2 && productionRunning) {\n productionRunning = false;\n stateChanged = true;\n}\n\nstate.productionStarted = productionRunning;\n\nconst stateMsg = stateChanged\n ? {\n _mode: \"production-state\",\n machineOnline: true,\n productionStarted: productionRunning\n }\n : null;\n\n// =============================================\n// EARLY EXIT CONDITIONS\n// =============================================\nconst cavities = Number(\n activeOrder?.cavities ??\n state.moldByWorkOrder?.[activeOrder?.id]?.active ??\n state.lastMoldActive ??\n settings.moldActive ??\n global.get(\"moldActive\") ??\n 0\n);\n\nnode.warn(`[CYCLE-DBG] cur=${current} prev=${prev} wo=${activeOrder?.id} cav=${cavities} | aWO.cav=${activeOrder?.cavities} mBWO=${state.moldByWorkOrder?.[activeOrder?.id]?.active} lMA=${state.lastMoldActive} sMA=${settings.moldActive} gMA=${global.get(\"moldActive\")}`);\n\nif (!activeOrder || !activeOrder.id || !Number.isFinite(cavities) || cavities <= 0) {\n return finalize([null, stateMsg, { _triggerKPI: true }, null]);\n}\n\nactiveOrder.cavities = cavities;\n\nif (!trackingEnabled) {\n return finalize([null, stateMsg, { _triggerKPI: true }, null]);\n}\n\n// Only count cycles on rising edge (0→1)\nif (prev === 1 || current !== 1) {\n return finalize([null, stateMsg, { _triggerKPI: true }, null]);\n}\n\n// =============================================\n// CYCLE COUNTING (only reached on 0→1 transition)\n// =============================================\n\nconst lastCompletion = state.lastCycleCompletionTime;\nif (lastCompletion) {\n const actualCycleTime = (now - lastCompletion) / 1000; // seconds\n state.lastActualCycleTime = actualCycleTime;\n}\n\nlet cycles = Number(state.cycleCount || 0) + 1;\nstate.cycleCount = cycles;\n// Track when this cycle completed (for gap analysis)\nstate.lastCycleCompletionTime = now;\n\n// Clear startup mode after first real cycle\nif (state.kpiStartupMode) {\n state.kpiStartupMode = false;\n node.warn('[MACHINE CYCLE] First cycle - cleared kpiStartupMode');\n}\n\nstate.lastMachineCycleTime = now;\nstate.saveKpis = 1\n\n// =============================================\n// PRODUCTION METRICS\n// =============================================\nconst scrapTotal = Number(activeOrder.scrapParts) || 0;\nconst totalProduced = cycles * cavities;\nconst produced = Math.max(0, totalProduced - scrapTotal);\nconst target = Number(activeOrder.target) || 0;\nconst progress = target > 0 ? Math.min(100, Math.round((produced / target) * 100)) : 0;\n\nactiveOrder.goodParts = produced;\nactiveOrder.progressPercent = progress;\nactiveOrder.lastUpdateIso = new Date().toISOString();\nstate.activeWorkOrder = activeOrder;\n\n// =============================================\n// SCRAP PROMPT CHECK\n// =============================================\nconst promptIssued = state.scrapPromptIssuedFor || null;\nif (!promptIssued && target > 0 && produced >= target) {\n state.scrapPromptIssuedFor = activeOrder.id;\n msg._mode = \"scrap-prompt\";\n msg.scrapPrompt = {\n id: activeOrder.id,\n sku: activeOrder.sku || \"\",\n target,\n produced\n };\n return finalize([null, msg, null, null]);\n}\n\n// =============================================\n// DATABASE UPDATE\n// =============================================\nconst dbMsg = {\n _mode: \"cycle\",\n cycle: {\n id: activeOrder.id,\n sku: activeOrder.sku || \"\",\n target,\n goodParts: produced,\n scrapParts: scrapTotal,\n cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0),\n progressPercent: progress,\n lastUpdateIso: activeOrder.lastUpdateIso,\n machineOnline: true,\n productionStarted: productionRunning\n },\n topic: \"UPDATE work_orders SET good_parts = ?, progress_percent = ?, updated_at = NOW() WHERE work_order_id = ?\",\n payload: [produced, progress, activeOrder.id]\n};\n\nconst kpiTrigger = { _triggerKPI: true };\n\nconst persistWorkOrder = {\n topic: \"UPDATE work_orders SET cycle_count = ?, good_parts = ?, scrap_parts = ?, progress_percent = ?, updated_at = NOW() WHERE work_order_id = ?\",\n payload: [cycles, produced, scrapTotal, progress, activeOrder.id]\n};\n\ndbMsg.cycleRow = {\n tsMs: now, // ms epoch\n cycle_count: cycles, // cycle counter\n actual_cycle_time: Number(state.lastActualCycleTime || 0),\n theoretical_cycle_time: Number(activeOrder.cycleTime || 0),\n work_order_id: String(activeOrder.id),\n sku: String(activeOrder.sku || \"\"),\n cavities: cavities,\n good_delta: cavities, // optional (1 cycle worth)\n scrap_total: scrapTotal, // optional\n};\n\n\nreturn finalize([dbMsg, stateMsg, kpiTrigger, persistWorkOrder]);", + "outputs": 4, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1860, + "y": 220, + "wires": [ + [ + "0319de4ecd45bdbf", + "d685745ab029bddb" + ], + [ + "cf7c59785d89fa2a", + "5c86dbb1f8044c9b" + ], + [], + [ + "30be157b60fc55fd" + ] + ] + }, + { + "id": "0319de4ecd45bdbf", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "c95c05b78d8464c5", + "name": "link out 5", + "mode": "link", + "links": [ + "6f993b8cb99de446" + ], + "x": 2015, + "y": 200, + "wires": [] + }, + { + "id": "6f993b8cb99de446", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link in 5", + "links": [ + "0319de4ecd45bdbf" + ], + "x": 1525, + "y": 480, + "wires": [ + [ + "037e4825e2537ed2" + ] + ] + }, + { + "id": "cf7c59785d89fa2a", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "c95c05b78d8464c5", + "name": "link out 6", + "mode": "link", + "links": [ + "86abdbe356524296" + ], + "x": 2035, + "y": 240, + "wires": [] + }, + { + "id": "86abdbe356524296", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link in 6", + "links": [ + "cf7c59785d89fa2a" + ], + "x": 1755, + "y": 580, + "wires": [ + [ + "97399876ca829d1a" + ] + ] + }, + { + "id": "cb972412abfe909f", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link out 7", + "mode": "link", + "links": [ + "de82c1e664a73bfb" + ], + "x": 2305, + "y": 600, + "wires": [] + }, + { + "id": "de82c1e664a73bfb", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "link in 7", + "links": [ + "cb972412abfe909f" + ], + "x": 535, + "y": 420, + "wires": [ + [ + "6adb9fd7e98f4274" + ] + ] + }, + { + "id": "6b1a37faf14b1a55", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "name": "link out 8", + "mode": "link", + "links": [ + "a51caffa7492c805" + ], + "x": 955, + "y": 660, + "wires": [] + }, + { + "id": "a51caffa7492c805", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "link in 8", + "links": [ + "6b1a37faf14b1a55", + "b45e6bfabde1f79c" + ], + "x": 275, + "y": 480, + "wires": [ + [ + "372edd599e1b2fdd" + ] + ] + }, + { + "id": "ddf6d786b5a7e682", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Calculate KPIs", + "func": "// ========================================\n// KPI HEARTBEAT - Continuous OEE calculation\n// Runs every second from a dedicated inject node\n// ========================================\n\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\n\n// 1) Basic guards: only run when tracking an active work order\nconst trackingEnabled = state.trackingEnabled || false;\nconst activeOrder = state.activeWorkOrder || {};\nif (!trackingEnabled || !activeOrder.id) {\n return null;\n}\n\n// Detect work-order change and restore/reset KPI counters\nconst currentId = activeOrder.id;\nif (state.kpiOrderId !== currentId) {\n const byOrder = state.kpiByWorkOrder || {};\n const saved = byOrder[currentId];\n\n if (saved && state.activeOrderHasProgress) {\n state.productionStartTime = saved.productionStartTime;\n state.runSeconds = saved.runSeconds;\n state.stopSeconds = saved.stopSeconds;\n state.kpiStartupMode = !!saved.kpiStartupMode;\n state.kpiLastTick = null; // avoid huge dt jump\n } else {\n state.productionStartTime = Date.now();\n state.runSeconds = 0;\n state.stopSeconds = 0;\n state.kpiStartupMode = true;\n state.kpiLastTick = null;\n }\n\n state.kpiOrderId = currentId;\n state.perf = { lastCycleCount: 0, runSeconds: 0 };\n\n}\n\n\n// Production must have a start time\n//const productionStartTime = state.productionStartTime;\n//if (!productionStartTime) {\n// return null;\n//}\n\n// Optional startup mode: keep 100/100/100/100 until first real cycle\nlet kpiStartupMode = !!state.kpiStartupMode;\n\nif (kpiStartupMode && Number(state.cycleCount || 0) > 0) {\n state.kpiStartupMode = false;\n kpiStartupMode = false;\n}\n\n// 2) Time step (dt) since last KPI tick\nconst now = Date.now();\nlet lastTick = state.kpiLastTick;\nlet dt;\nif (!lastTick) {\n dt = 0; // first run: don't integrate any time yet\n} else {\n dt = (now - lastTick) / 1000;\n}\nstate.kpiLastTick = now;\n\n// Clamp weird dt values (e.g. after long pause)\nif (dt < 0 || dt > 5) {\n dt = 1;\n}\nlet productionStartTime = state.productionStartTime;\nif (!productionStartTime) {\n // if we’re tracking a valid order, start the clock now\n productionStartTime = now;\n state.productionStartTime = productionStartTime;\n}\n\n// 3) Scheduled production time so far (denominator for Availability)\nlet elapsedSeconds = (now - productionStartTime) / 1000;\nif (elapsedSeconds < 0) elapsedSeconds = 0;\n\nconst plannedProductionTime = Number(state.plannedProductionTime || 0);\n\n// scheduledSeconds = time in the shift that *should* be production\nlet scheduledSeconds = elapsedSeconds;\nif (plannedProductionTime > 0) {\n scheduledSeconds = Math.min(elapsedSeconds, plannedProductionTime);\n}\n\n// 4) Determine machine state from last cycle timestamp\nconst lastCycleTime = state.lastMachineCycleTime || null;\nlet machineState = state.machineState || \"IDLE\";\n\nif (lastCycleTime) {\n const sinceLastCycle = (now - lastCycleTime) / 1000;\n const idealCycleTime = Number(activeOrder.cycleTime) || 1;\n const thresholdMultiplier = Number(settings.thresholdMultiplier || 1.5);\n const stopThreshold = idealCycleTime * thresholdMultiplier;\n\n machineState = (sinceLastCycle <= stopThreshold) ? \"RUNNING\" : \"STOPPED\";\n state.machineState = machineState;\n\n if (kpiStartupMode && sinceLastCycle > stopThreshold) {\n state.kpiStartupMode = false;\n kpiStartupMode = false;\n }\n}\n// ---- Shift-based availability (daily) ----\nconst shifts = settings.shifts || [{ start: '06:00', end: '15:00' }];\nconst shiftChangeComp = Number(settings.shiftChangeCompensation ?? 10);\nconst lunchBreak = Number(settings.lunchBreakMinutes ?? 30);\n\nfunction hmToMinutes(hm) {\n const parts = String(hm || '00:00').split(':').map(Number);\n return (parts[0] || 0) * 60 + (parts[1] || 0);\n}\nfunction isInShift(nowMs) {\n const d = new Date(nowMs);\n const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0).getTime();\n return shifts.some(s => {\n let startM = hmToMinutes(s.start);\n let endM = hmToMinutes(s.end);\n let start = dayStart + startM * 60000;\n let end = dayStart + endM * 60000;\n if (end <= start) end += 24 * 60 * 60000; // overnight\n return nowMs >= start && nowMs <= end;\n });\n}\nfunction plannedDaySeconds() {\n let total = 0;\n shifts.forEach(s => {\n let startM = hmToMinutes(s.start);\n let endM = hmToMinutes(s.end);\n if (endM <= startM) endM += 24 * 60;\n total += (endM - startM) * 60;\n });\n const compensation = shifts.length * shiftChangeComp * 60;\n const lunch = lunchBreak * 60;\n return Math.max(0, total - compensation - lunch);\n}\n\nconst d = new Date(now);\nconst dayKey = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;\nif (state.availDayKey !== dayKey) {\n state.availDayKey = dayKey;\n state.availDowntimeSeconds = 0;\n}\n\nconst inShift = isInShift(now);\nconst plannedSeconds = plannedDaySeconds();\nif (inShift && dt > 0 && machineState !== \"RUNNING\") {\n state.availDowntimeSeconds = Number(state.availDowntimeSeconds || 0) + dt;\n}\nconst availabilityPct = plannedSeconds > 0\n ? ((plannedSeconds - (state.availDowntimeSeconds || 0)) / plannedSeconds) * 100\n : 0;\n\n\n// 5) Integrate run / stop time (numerators)\n// We keep these as smooth, per-second counters\nlet runSeconds = Number(state.runSeconds || 0);\nlet stopSeconds = Number(state.stopSeconds || 0);\n\n// During startup mode (before first cycle), don't integrate anything\nif (!kpiStartupMode && dt > 0 && scheduledSeconds > 0) {\n if (machineState === \"RUNNING\") {\n runSeconds += dt;\n } else if (machineState === \"STOPPED\") {\n stopSeconds += dt;\n }\n}\n\nstate.runSeconds = runSeconds;\nstate.stopSeconds = stopSeconds;\n\n// 6) Pull counters for Performance & Quality\nconst cycleCount = Number(state.cycleCount || 0);\nconst cavities = Number(\n activeOrder.cavities ??\n (state.moldByWorkOrder?.[activeOrder.id]?.active) ??\n state.lastMoldActive ??\n settings.moldActive ??\n 0\n);\nconst totalParts = cycleCount * cavities;\nconst totalCycles = cycleCount;\n\n\nconst scrapTotal = Number(activeOrder.scrapParts) || 0;\nconst goodParts = Math.max(0, totalParts - scrapTotal);\n\nconst idealCycleTime = Number(activeOrder.cycleTime) || 1;\n\n// 7) Compute KPIs\n\n// Helper to keep values sane\nfunction clampPercent(v) {\n if (!isFinite(v) || isNaN(v)) return 0;\n return Math.max(0, Math.min(100, v));\n}\n\nlet availability = 0;\nlet performance = 0;\nlet quality = 100;\nlet oee = 0;\n\n// Startup mode: skip KPI emit until first real cycle/state transition\nif (kpiStartupMode) {\n global.set(\"state\", state);\n return null;\n} else {\n // Availability = Run Time / Scheduled Production Time\n availability = availabilityPct;\n\n\n // Performance = (Ideal Cycle Time × Cycles) / Sum of actual cycle times\n const actualCycleTime = Number(state.lastActualCycleTime || 0);\n state.perf = state.perf || { lastCycleCount: 0, runSeconds: 0 };\n\n // Use same threshold you use for slow-cycle\n const perfThreshold = idealCycleTime * Number(settings.thresholdMultiplier || 1.5);\n\n if (cycleCount > state.perf.lastCycleCount && actualCycleTime > 0) {\n // Cap at threshold so time beyond becomes downtime, not performance loss\n const perfCycleTime = Math.min(actualCycleTime, perfThreshold);\n state.perf.runSeconds += perfCycleTime;\n state.perf.lastCycleCount = cycleCount;\n }\n\n if (state.perf.runSeconds > 0 && cycleCount > 0) {\n const idealTime = idealCycleTime * cycleCount;\n performance = (idealTime / state.perf.runSeconds) * 100;\n }\n\n // Quality = Good Parts / Total Parts\n if (totalParts > 0) {\n quality = (goodParts / totalParts) * 100;\n }\n\n availability = clampPercent(availability);\n performance = clampPercent(performance);\n quality = clampPercent(quality);\n\n // OEE = A × P × Q\n oee = (availability * performance * quality) / 10000;\n}\n\n// Clamp & round to 1 decimal\noee = clampPercent(oee);\n\nmsg.kpis = {\n availability: Math.round(availability * 10) / 10,\n performance: Math.round(performance * 10) / 10,\n quality: Math.round(quality * 10) / 10,\n oee: Math.round(oee * 10) / 10\n};\n\nstate.currentKPIs = msg.kpis;\nstate.kpiByWorkOrder = state.kpiByWorkOrder || {};\nstate.kpiByWorkOrder[activeOrder.id] = {\n productionStartTime: state.productionStartTime,\n runSeconds: state.runSeconds,\n stopSeconds: state.stopSeconds,\n kpiStartupMode: state.kpiStartupMode,\n kpiLastTick: state.kpiLastTick\n};\nglobal.set(\"state\", state);\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1380, + "y": 680, + "wires": [ + [ + "97399876ca829d1a", + "35542f4dc31c3a96", + "ea175b8208d85ed5", + "c323fd9c710bcded", + "c48444f30fc71bae" + ] + ] + }, + { + "id": "851a0e837a94ed78", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1f2e45c551f40615", + "name": "Process Alert for DB", + "func": "// Process incoming alert\nif (msg.payload && msg.payload.action === 'alert') {\n const alert = msg.payload;\n\n const tsMs = typeof alert.tsMs === \"number\" ? alert.tsMs : Date.now();\n const timestamp = new Date(tsMs).toISOString().slice(0, 19).replace('T', ' ');\n\n // Prepare INSERT query\n const alertType = (alert.type || 'Unknown').replace(/'/g, \"''\"); // Escape quotes\n const description = (alert.description || '').replace(/'/g, \"''\"); // Escape quotes\n\n msg.topic = `\n INSERT INTO alerts_log (timestamp, alert_type, description)\n VALUES ('${timestamp}', '${alertType}', '${description}')\n `;\n\n node.status({\n fill: 'green',\n shape: 'dot',\n text: `Logging: ${alertType}`\n });\n\n // Store original message for passthrough\n msg._originalAlert = alert;\n\n return msg;\n}\n\nreturn null;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 440, + "y": 860, + "wires": [ + [ + "6f2897368cb48370" + ] + ] + }, + { + "id": "6f2897368cb48370", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "g": "1f2e45c551f40615", + "mydb": "fc9634aabefee16b", + "name": "Log Alert to DB", + "x": 640, + "y": 860, + "wires": [ + [] + ] + }, + { + "id": "76598272c6e67323", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "link in 9", + "links": [ + "ca6e4da8d0c5f853", + "52501193c1677944" + ], + "x": 275, + "y": 400, + "wires": [ + [ + "6feab0f06c780f4c" + ] + ] + }, + { + "id": "ca6e4da8d0c5f853", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link out 9", + "mode": "link", + "links": [ + "76598272c6e67323" + ], + "x": 1935, + "y": 660, + "wires": [] + }, + { + "id": "35542f4dc31c3a96", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Record KPI History", + "func": "// Complete Record KPI History function with robust initialization and averaging\n\nconst state = global.get(\"state\") || {};\nconst saveState = () => global.set(\"state\", state);\n\n// ========== INITIALIZATION ==========\n// Initialize buffer\nlet buffer = state.kpiBuffer;\nif (!buffer || !Array.isArray(buffer)) {\n buffer = [];\n state.kpiBuffer = buffer;\n node.warn('[KPI History] Initialized kpiBuffer');\n}\n\n// Initialize last record time\nlet lastRecordTime = state.lastKPIRecordTime;\nif (!lastRecordTime || typeof lastRecordTime !== 'number') {\n // Set to 1 minute ago to ensure immediate recording on startup\n lastRecordTime = Date.now() - 60000;\n state.lastKPIRecordTime = lastRecordTime;\n node.warn('[KPI History] Initialized lastKPIRecordTime');\n}\n\n// ========== ACCUMULATE ==========\nconst kpis = msg.payload?.kpis || msg.kpis;\nif (!kpis) {\n node.warn('[KPI History] No KPIs in message, skipping');\n saveState();\n return null;\n}\n\nbuffer.push({\n tsMs: Date.now(),\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n});\n\n// Prevent buffer from growing too large (safety limit)\nif (buffer.length > 100) {\n buffer = buffer.slice(-60); // Keep last 60 entries\n node.warn('[KPI History] Buffer exceeded 100 entries, trimmed to 60');\n}\n\nstate.kpiBuffer = buffer;\nsaveState();\n\n// ========== CHECK IF TIME TO RECORD ==========\nconst now = Date.now();\nconst timeSinceLastRecord = now - lastRecordTime;\nconst ONE_MINUTE = 60 * 1000;\n\nif (timeSinceLastRecord < ONE_MINUTE) {\n // Not time to record yet\n return null; // Don't send to charts yet\n}\n\n// ========== CALCULATE AVERAGES ==========\nif (buffer.length === 0) {\n node.warn('[KPI History] Buffer empty at recording time, skipping');\n return null;\n}\n\nconst avg = {\n oee: buffer.reduce((sum, d) => sum + d.oee, 0) / buffer.length,\n availability: buffer.reduce((sum, d) => sum + d.availability, 0) / buffer.length,\n performance: buffer.reduce((sum, d) => sum + d.performance, 0) / buffer.length,\n quality: buffer.reduce((sum, d) => sum + d.quality, 0) / buffer.length\n};\n\nnode.warn(`[KPI History] Recording averaged KPIs from ${buffer.length} samples: OEE=${avg.oee.toFixed(1)}%`);\n\n// ========== RECORD TO HISTORY ==========\n// Load history arrays\nlet oeeHist = state.realOEE || [];\nlet availHist = state.realAvailability || [];\nlet perfHist = state.realPerformance || [];\nlet qualHist = state.realQuality || [];\n\n// Append averaged values\noeeHist.push({ tsMs: now, value: Math.round(avg.oee * 10) / 10 });\navailHist.push({ tsMs: now, value: Math.round(avg.availability * 10) / 10 });\nperfHist.push({ tsMs: now, value: Math.round(avg.performance * 10) / 10 });\nqualHist.push({ tsMs: now, value: Math.round(avg.quality * 10) / 10 });\n\n// Trim arrays (avoid memory explosion)\noeeHist = oeeHist.slice(-300);\navailHist = availHist.slice(-300);\nperfHist = perfHist.slice(-300);\nqualHist = qualHist.slice(-300);\n\n// Save\nstate.realOEE = oeeHist;\nstate.realAvailability = availHist;\nstate.realPerformance = perfHist;\nstate.realQuality = qualHist;\n\n// Update state\nstate.lastKPIRecordTime = now;\nstate.kpiBuffer = []; // Clear buffer\nsaveState();\n\n// Send to graphs\nreturn {\n topic: \"chartsData\",\n payload: {\n oee: oeeHist,\n availability: availHist,\n performance: perfHist,\n quality: qualHist\n }\n};\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1750, + "y": 660, + "wires": [ + [ + "ca6e4da8d0c5f853" + ] + ] + }, + { + "id": "2139786b0733335d", + "type": "inject", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "Init on Deploy", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 380, + "y": 220, + "wires": [ + [ + "59c4e2cd83fd1876" + ] + ] + }, + { + "id": "59c4e2cd83fd1876", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "Initialize Global Variables", + "func": "node.warn(\"[INIT] Initializing global variables\");\n\nconst settings = global.get(\"settings\") || {};\nconst state = global.get(\"state\") || {};\n\nlet fileSettings = null;\ntry {\n fileSettings = global.get(\"settings\", \"file\");\n} catch (err) {\n fileSettings = null;\n}\nif (fileSettings && typeof fileSettings === \"object\") {\n if (settings.moldTotal == null && fileSettings.moldTotal != null) {\n settings.moldTotal = fileSettings.moldTotal;\n }\n if (settings.moldActive == null && fileSettings.moldActive != null) {\n settings.moldActive = fileSettings.moldActive;\n }\n if (!Array.isArray(settings.shifts) && Array.isArray(fileSettings.shifts)) {\n settings.shifts = fileSettings.shifts;\n }\n if (settings.shiftChangeCompensation == null && fileSettings.shiftChangeCompensation != null) {\n settings.shiftChangeCompensation = fileSettings.shiftChangeCompensation;\n }\n if (settings.lunchBreakMinutes == null && fileSettings.lunchBreakMinutes != null) {\n settings.lunchBreakMinutes = fileSettings.lunchBreakMinutes;\n }\n if (settings.thresholdMultiplier == null && fileSettings.thresholdMultiplier != null) {\n settings.thresholdMultiplier = fileSettings.thresholdMultiplier;\n }\n}\n\nlet moldCache = null;\ntry {\n moldCache = global.get(\"moldCache\", \"file\");\n} catch (err) {\n moldCache = null;\n}\nif (moldCache && typeof moldCache === \"object\") {\n if (!Number.isFinite(state.lastMoldActive) && Number.isFinite(moldCache.lastMoldActive)) {\n state.lastMoldActive = moldCache.lastMoldActive;\n }\n if (!Number.isFinite(state.lastMoldTotal) && Number.isFinite(moldCache.lastMoldTotal)) {\n state.lastMoldTotal = moldCache.lastMoldTotal;\n }\n if (!state.moldByWorkOrder && moldCache.moldByWorkOrder && typeof moldCache.moldByWorkOrder === \"object\") {\n state.moldByWorkOrder = moldCache.moldByWorkOrder;\n }\n}\n\n// KPI Buffer for averaging\nif (!Array.isArray(state.kpiBuffer)) {\n state.kpiBuffer = [];\n node.warn(\"[INIT] Set state.kpiBuffer to []\");\n}\n\n// Last KPI record time - set to 1 min ago for immediate first record\nif (typeof state.lastKPIRecordTime !== \"number\") {\n state.lastKPIRecordTime = Date.now() - 60000;\n node.warn(\"[INIT] Set state.lastKPIRecordTime\");\n}\n\n// Last machine cycle time - set to now to prevent immediate 0% availability\nif (typeof state.lastMachineCycleTime !== \"number\") {\n state.lastMachineCycleTime = Date.now();\n node.warn(\"[INIT] Set state.lastMachineCycleTime to prevent 0% availability on startup\");\n}\n\n// Last KPI values\nif (!state.lastKPIValues || typeof state.lastKPIValues !== \"object\") {\n state.lastKPIValues = {};\n node.warn(\"[INIT] Set state.lastKPIValues to {}\");\n}\n\n// KPI Startup Mode - ensure clean state on deploy\nstate.kpiStartupMode = false;\nnode.warn(\"[INIT] Set state.kpiStartupMode to false\");\n\n// Tracking flags - ensure clean state\nif (typeof state.trackingEnabled !== \"boolean\") {\n state.trackingEnabled = false;\n}\nif (typeof state.productionStarted !== \"boolean\") {\n state.productionStarted = false;\n}\n\n// Settings defaults\nif (!Array.isArray(settings.shifts) || settings.shifts.length === 0) {\n settings.shifts = [{ start: \"06:00\", end: \"15:00\" }];\n node.warn(\"[INIT] Set default shift: 06:00-15:00\");\n}\n\nif (typeof settings.shiftChangeCompensation !== \"number\") {\n settings.shiftChangeCompensation = 10;\n}\n\nif (typeof settings.lunchBreakMinutes !== \"number\") {\n settings.lunchBreakMinutes = 30;\n}\n\nif (typeof settings.thresholdMultiplier !== \"number\") {\n settings.thresholdMultiplier = 1.5;\n}\n\n// State defaults\nif (typeof state.operatingTime !== \"number\") {\n state.operatingTime = 0;\n}\n\nif (typeof state.stopTime !== \"number\") {\n state.stopTime = 0;\n}\n\nif (typeof state.plannedProductionTime !== \"number\") {\n state.plannedProductionTime = 0;\n}\n\nif (!Object.prototype.hasOwnProperty.call(state, \"lastCycleCompletionTime\")) {\n state.lastCycleCompletionTime = null;\n}\n\nglobal.set(\"settings\", settings);\ntry {\n global.set(\"settings\", settings, \"file\");\n} catch (err) {\n // ignore if file store is not configured\n}\nglobal.set(\"state\", state);\nconst moldCacheOut = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n};\nglobal.set(\"moldCache\", moldCacheOut);\ntry {\n global.set(\"moldCache\", moldCacheOut, \"file\");\n} catch (err) {\n // ignore if file store is not configured\n}\n\nnode.warn(\"[INIT] Global variable initialization complete\");\n\n// Trigger restore-session to check for running work orders\nconst restoreMsg = { action: \"restore-session\" };\nreturn [null, restoreMsg];", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 630, + "y": 220, + "wires": [ + [], + [ + "c7a8bb7adab6838e" + ] + ] + }, + { + "id": "30be157b60fc55fd", + "type": "switch", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "DB Guard (Cycles)", + "property": "topic", + "propertyType": "msg", + "rules": [ + { + "t": "istype", + "v": "string", + "vt": "string" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 1690, + "y": 380, + "wires": [ + [ + "037e4825e2537ed2" + ] + ] + }, + { + "id": "52501193c1677944", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "8b73333804b30ee0", + "name": "link out 10", + "mode": "link", + "links": [ + "76598272c6e67323" + ], + "x": 1955, + "y": 80, + "wires": [] + }, + { + "id": "4bb96d61f9e3bba2", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Progress Check Handler", + "func": "// Handle DB result from start-work-order progress check\nconst state = global.get(\"state\") || {};\nconst persistMoldCache = () => {\n const cache = {\n lastMoldActive: state.lastMoldActive ?? null,\n lastMoldTotal: state.lastMoldTotal ?? null,\n moldByWorkOrder: state.moldByWorkOrder || {}\n };\n global.set(\"moldCache\", cache);\n try {\n global.set(\"moldCache\", cache, \"file\");\n } catch (err) {\n // ignore if file store is not configured\n }\n};\n\nconst persistState = () => {\n global.set(\"state\", state);\n};\n\nif (msg._mode === \"start-check-progress\") {\n const order = flow.get(\"pendingWorkOrder\");\n\n if (!order || !order.id) {\n node.error(\"No pending work order found\", msg);\n return [null, null];\n }\n\n // Get progress from DB query result\n const dbRow = (Array.isArray(msg.payload) && msg.payload.length > 0) ? msg.payload[0] : null;\n const cycleCount = dbRow ? (Number(dbRow.cycle_count) || 0) : 0;\n const goodParts = dbRow ? (Number(dbRow.good_parts) || 0) : 0;\n const scrapParts = dbRow ? (Number(dbRow.scrap_parts) || 0) : 0;\n const targetQty = dbRow ? (Number(dbRow.target_qty) || 0) : (Number(order.target) || 0);\n const cavitiesTotal = dbRow ? (Number(dbRow.cavities_total) || 0) : 0;\n const cavitiesActive = dbRow ? (Number(dbRow.cavities_active) || 0) : 0;\n\n const hasProgress = cycleCount > 0 || (goodParts + scrapParts) > 0;\n state.activeOrderHasProgress = hasProgress;\n state.activeOrderId = order.id;\n\n\n node.warn(`[PROGRESS-CHECK] WO ${order.id}: cycles=${cycleCount}, good=${goodParts}, target=${targetQty}`);\n\n // Check if work order has existing progress\n if (hasProgress) {\n // Work order has progress - send prompt to UI\n node.warn(`[PROGRESS-CHECK] Work order has existing progress - sending prompt to UI`);\n\n if (Number.isFinite(cavitiesActive) && cavitiesActive > 0) {\n order.cavities = cavitiesActive;\n }\n if (Number.isFinite(cavitiesTotal) && cavitiesTotal > 0) {\n order.cavities_total = cavitiesTotal;\n }\n\n const promptMsg = {\n _mode: \"resume-prompt\",\n topic: \"resumePrompt\",\n payload: {\n id: order.id,\n sku: order.sku || \"\",\n cycleCount: cycleCount,\n goodParts: goodParts,\n targetQty: targetQty,\n progressPercent: targetQty > 0 ? Math.round((goodParts / targetQty) * 100) : 0,\n // Include full order object for resume/restart actions\n order: { ...order, cycleCount: cycleCount, goodParts: goodParts, scrapParts: scrapParts }\n }\n };\n\n persistState();\n persistMoldCache();\n return [null, promptMsg];\n } else {\n // No existing progress - proceed with normal start\n // But still use DB values (even if 0) to ensure DB is source of truth\n node.warn(`[PROGRESS-CHECK] No existing progress - proceeding with normal start`);\n\n state.activeOrderHasProgress = false;\n\n // Update order object with DB values (makes DB the source of truth)\n order.cycleCount = cycleCount; // Will be 0 from DB\n order.goodParts = goodParts; // Will be 0 from DB\n order.scrapParts = scrapParts; // Will be 0 from DB\n order.target = targetQty; // From DB\n\n const moldByWorkOrder = state.moldByWorkOrder || {};\n const mapped = moldByWorkOrder[order.id] || {};\n const resolvedCavities = Number(\n order.cavities ?? cavitiesActive ?? mapped.active ?? state.lastMoldActive ?? 0\n );\n const resolvedTotal = Number(\n order.cavities_total ?? cavitiesTotal ?? mapped.total ?? state.lastMoldTotal ?? 0\n );\n if (Number.isFinite(resolvedCavities) && resolvedCavities > 0) {\n order.cavities = resolvedCavities;\n state.lastMoldActive = resolvedCavities;\n }\n if (Number.isFinite(resolvedTotal) && resolvedTotal > 0) {\n order.cavities_total = resolvedTotal;\n state.lastMoldTotal = resolvedTotal;\n }\n if (Number.isFinite(resolvedCavities) && resolvedCavities > 0) {\n moldByWorkOrder[order.id] = {\n total: Number.isFinite(resolvedTotal) && resolvedTotal > 0 ? resolvedTotal : (state.lastMoldTotal ?? null),\n active: resolvedCavities\n };\n state.moldByWorkOrder = moldByWorkOrder;\n }\n\n const startMsg = {\n _mode: \"start\",\n startOrder: order,\n topic: \"UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END, cavities_total = COALESCE(NULLIF(?,0), cavities_total), cavities_active = COALESCE(NULLIF(?,0), cavities_active) WHERE status <> 'DONE'\",\n payload: [order.id, order.id, Number(order.cavities_total || 0), Number(order.cavities || 0)]\n };\n\n // Initialize global state with DB values (even if 0)\n state.activeWorkOrder = order;\n persistState();\n state.cycleCount = cycleCount;\n persistState(); // Use DB value instead of hardcoded 0\n flow.set(\"lastMachineState\", 0);\n state.scrapPromptIssuedFor = null;\n persistState();\n persistMoldCache();\n\n node.warn(`[PROGRESS-CHECK] Initialized from DB: cycles=${cycleCount}, good=${goodParts}, scrap=${scrapParts}`);\n\n return [startMsg, null];\n }\n}\n\n// Pass through all other messages\nreturn [msg, null];", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1910, + "y": 540, + "wires": [ + [ + "97399876ca829d1a" + ], + [ + "2872629594daf0ed" + ] + ] + }, + { + "id": "ea175b8208d85ed5", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Merge Cycle + KPI Data", + "func": "// ============================================================\n// DATA MERGER - Combines Cycle + KPI data for Anomaly Detector\n// ============================================================\n\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\n\n// Get KPIs from incoming message (from Calculate KPIs node)\nconst kpis = msg.kpis || msg.payload?.kpis || {};\n\n// Get cycle data from global context\nconst activeOrder = state.activeWorkOrder || {};\nconst cycleCount = state.cycleCount || 0;\nconst moldByWorkOrder = state.moldByWorkOrder || {};\nconst cavities = Number(\n activeOrder.cavities ??\n moldByWorkOrder[activeOrder.id]?.active ??\n state.lastMoldActive ??\n settings.moldActive ??\n 0\n);\nconst lastActualCycleTime = Number(state.lastActualCycleTime || 0);\n\n\n// Build cycle object with all necessary data\nconst cycle = {\n id: activeOrder.id,\n sku: activeOrder.sku || \"\",\n cycles: cycleCount,\n goodParts: Number(activeOrder.goodParts) || 0,\n scrapParts: Number(activeOrder.scrapParts) || 0,\n target: Number(activeOrder.target) || 0,\n cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0),\n progressPercent: Number(activeOrder.progressPercent) || 0,\n cavities: cavities,\n actualCycleTime: lastActualCycleTime\n\n};\n\n// Merge both into the message\nmsg.cycle = cycle;\nmsg.kpis = kpis;\n\n//node.warn(`[DATA MERGER] Merged cycle (count: ${cycleCount}) + KPIs (OEE: ${kpis.oee || 0}%) for anomaly detection`);\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1330, + "y": 480, + "wires": [ + [ + "04accd50a0fe3c90" + ] + ] + }, + { + "id": "04accd50a0fe3c90", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Anomaly Detector", + "func": "\n// ============================================================\n// ANOMALY DETECTOR - FIXED VERSION\n// Key fixes:\n// 1. Removed duplicate suppression that blocks microstop alerts\n// 2. Added periodic updates for active macrostops\n// 3. Improved logging for debugging\n// ============================================================\n\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\nconst anomaly = global.get(\"anomaly\") || {};\n\n// Suppress anomaly detection during mold change\nif (state.moldChange && state.moldChange.active) return null;\n\nconst cycle = msg.cycle || {};\nconst kpis = msg.kpis || {};\nconst activeOrder = state.activeWorkOrder || {};\nconst cycleCountNow = Number(cycle.cycles || 0);\n\n\n// Must have active work order to detect anomalies\nif (!activeOrder.id) {\n return null;\n}\n\nconst theoreticalCycleTime = Number(activeOrder.cycleTime) || 0;\nconst now = Date.now();\n\n// Get or initialize anomaly tracking state\nlet anomalyState = global.get(\"anomalyState\") || {\n lastCycleTime: Number(state.lastMachineCycleTime || now),\n //lastCycleCount: cycleCountNow,\n lastCycleCount: 0,\n activeStoppageEvent: null,\n oeeHistory: [],\n performanceHistory: [],\n qualityHistory: [],\n activeOeeDrop: false,\n oeeLowStreak: 0,\n activeQualitySpike: false,\n qualityHighStreak: 0,\n lastSlowCycleCount: 0,\n lastCycleEventCount: 0,\n lastStoppageUpdateMs: 0\n};\n// Reset anomaly cycle baseline when work order changes\nif (anomalyState.work_order_id && anomalyState.work_order_id !== activeOrder.id) {\n node.warn(`[RESET] Work order changed ${anomalyState.work_order_id} -> ${activeOrder.id}. Resetting anomalyState counters.`);\n anomalyState.lastCycleCount = 0;\n anomalyState.lastCycleEventCount = 0;\n anomalyState.lastSlowCycleCount = 0;\n anomalyState.activeStoppageEvent = null;\n anomalyState.lastStoppageUpdateMs = 0;\n}\nanomalyState.work_order_id = activeOrder.id;\n\nif (!isFinite(anomalyState.lastCycleTime)) {\n anomalyState.lastCycleTime = Number(state.lastMachineCycleTime || now);\n}\nif (!isFinite(anomalyState.lastCycleCount)) {\n anomalyState.lastCycleCount = cycleCountNow;\n}\n\nconst stateLastCycleTime = Number(state.lastMachineCycleTime || 0);\n//node.warn(`[TS CHECK] state.lastMachineCycleTime=${stateLastCycleTime} iso=${stateLastCycleTime ? new Date(stateLastCycleTime).toISOString() : 'n/a'}`);\nlet lastCycleTime = Number(anomalyState.lastCycleTime || 0);\nif (stateLastCycleTime > 0 && (lastCycleTime <= 0 || stateLastCycleTime > lastCycleTime)) {\n lastCycleTime = stateLastCycleTime;\n}\nconst prevCount = Number(anomalyState.lastCycleCount || 0);\n\n// If PLC/logic reset the counter (or new batch), re-baseline\nif (cycleCountNow > 0 && prevCount > 0 && cycleCountNow < prevCount) {\n node.warn(`[RESET] cycle counter reset detected: ${prevCount} -> ${cycleCountNow}. Re-baselining.`);\n anomalyState.lastCycleCount = cycleCountNow;\n anomalyState.lastCycleTime = stateLastCycleTime > 0 ? stateLastCycleTime : now;\n anomalyState.lastCycleEventCount = cycleCountNow;\n anomalyState.lastSlowCycleCount = cycleCountNow;\n anomalyState.activeStoppageEvent = null;\n anomalyState.lastStoppageUpdateMs = 0;\n}\nconst hasNewCycle = cycleCountNow > Number(anomalyState.lastCycleCount || 0);\n//node.warn(`[CYCLE CHECK PRE] cycleCountNow: ${cycleCountNow}, lastCycleCount: ${anomalyState.lastCycleCount}, hasNewCycle: ${cycleCountNow > Number(anomalyState.lastCycleCount || 0)}`);\n//node.warn(`[TS CHECK] stateLastCycleTime=${stateLastCycleTime} iso=${stateLastCycleTime ? new Date(stateLastCycleTime).toISOString() : 'n/a'}`);\nif (hasNewCycle) {\n anomalyState.lastCycleCount = cycleCountNow;\n anomalyState.lastCycleTime = stateLastCycleTime > 0 ? stateLastCycleTime : now;\n lastCycleTime = anomalyState.lastCycleTime;\n}\n//node.warn(`[CYCLE CHECK] cycleCountNow: ${cycleCountNow}, lastCycleCount: ${anomalyState.lastCycleCount}, hasNewCycle: ${hasNewCycle}`);\n//node.warn(`[CYCLE CHECK] msg.cycle.cycles: ${msg.cycle.cycles}, type: ${typeof msg.cycle.cycles}`);\n// Configuration\nconst OEE_THRESHOLD = settings.oeeAlertThreshold || 90;\nconst HISTORY_WINDOW = 20;\nconst QUALITY_SPIKE_THRESHOLD = 5;\nconst PERFORMANCE_THRESHOLD = settings.performanceThreshold || 85;\nconst microMultiplier = Number(settings.thresholdMultiplier || 1.5);\nconst macroMultiplier = Math.max(\n microMultiplier,\n Number(settings.macroStoppageMultiplier || 5)\n);\n\n// NEW: Update interval for macrostop notifications (default 10 seconds)\nconst updateIntervalMs = Number(settings.stoppageUpdateIntervalMs || 10000);\n\nconst detectedAnomalies = [];\n\nconst DEFAULT_DOWNTIME_REASON = {\n type: \"downtime\",\n categoryId: \"sin-clasificar\",\n categoryLabel: \"Sin clasificar\",\n detailId: \"pendiente\",\n detailLabel: \"Pendiente de clasificar\",\n reasonCode: \"PENDIENTE\",\n reasonText: \"Pendiente de clasificar\"\n};\n\nfunction applyDefaultDowntimeReason(event) {\n if (!event || (event.anomaly_type !== \"microstop\" && event.anomaly_type !== \"macrostop\")) {\n return event;\n }\n if (event.reason && typeof event.reason === \"object\") {\n return event;\n }\n return {\n ...event,\n reason: { ...DEFAULT_DOWNTIME_REASON }\n };\n}\n\n// ============================================================\n// TIER 1: CYCLE CLASSIFICATION + STOPPAGE WATCHDOG\n// ============================================================\nif (theoreticalCycleTime > 0) {\n const actualCycleTime = Number(cycle.actualCycleTime || 0);\n const currentCycleCount = Number(cycle.cycles || 0);\n const lastCycleEventCount = Number(\n anomalyState.lastCycleEventCount ?? anomalyState.lastSlowCycleCount ?? 0\n );\n\n const microThresholdSec = theoreticalCycleTime * microMultiplier;\n const macroThresholdSec = theoreticalCycleTime * macroMultiplier;\n const timeSinceLastCycleSec = lastCycleTime > 0 ? (now - lastCycleTime) / 1000 : 0;\n\n // DEBUG LOGGING\n // node.warn(`[DEBUG] actualCycleTime: ${actualCycleTime}s, theoretical: ${theoreticalCycleTime}s`);\n // node.warn(`[DEBUG] microThreshold: ${microThresholdSec}s, macroThreshold: ${macroThresholdSec}s`);\n // node.warn(`[DEBUG] timeSinceLastCycle: ${timeSinceLastCycleSec}s, hasNewCycle: ${hasNewCycle}`);\n\n let resolvedStoppageThisCycle = false;\n\n // Resolve any active stoppage when a new cycle arrives\n if (anomalyState.activeStoppageEvent && hasNewCycle) {\n const resolvedDurationSec = actualCycleTime > 0 ? actualCycleTime : timeSinceLastCycleSec;\n\n detectedAnomalies.push({\n ...anomalyState.activeStoppageEvent,\n status: \"resolved\",\n resolved_at: now,\n auto_resolved: true,\n data: {\n ...anomalyState.activeStoppageEvent.data,\n stoppage_duration_seconds: Math.round(resolvedDurationSec)\n },\n tsMs: now\n });\n\n node.warn(`[RESOLVED] Stoppage resolved: ${anomalyState.activeStoppageEvent.anomaly_type}, duration: ${resolvedDurationSec}s`);\n anomalyState.activeStoppageEvent = null;\n anomalyState.lastStoppageUpdateMs = 0;\n resolvedStoppageThisCycle = true;\n }\n\n if (hasNewCycle) {\n //node.warn(`[CYCLE RESUME] New cycle detected! Count: ${cycleCountNow}, lastCount: ${anomalyState.lastCycleCount}`);\n //node.warn(`[CYCLE RESUME] lastCycleTime updated from ${lastCycleTime} to ${anomalyState.lastCycleTime}`);\n //node.warn(`[CYCLE RESUME] activeStoppageEvent cleared: ${anomalyState.activeStoppageEvent === null}`);\n }\n\n // Add before the periodic update section (around line ~270)\n // if (anomalyState.activeStoppageEvent) {\n // node.warn(`[WATCHDOG] Active stoppage exists: ${anomalyState.activeStoppageEvent.anomaly_type}`);\n //node.warn(`[WATCHDOG] timeSinceLastCycle: ${timeSinceLastCycleSec}s, lastCycleTime: ${lastCycleTime}, now: ${now}`);\n // }\n\n // Per-cycle classification (uses actualCycleTime)\n if (actualCycleTime > 0 && currentCycleCount > lastCycleEventCount) {\n let cycleEvent = null;\n\n const SLOW_MARGIN = Number(settings.slowCycleMarginPercent ?? 5); // 5% default\n\n if (\n actualCycleTime > 0 &&\n actualCycleTime > theoreticalCycleTime * (1 + SLOW_MARGIN / 100) &&\n actualCycleTime < microThresholdSec\n ) {\n const deltaPercent = ((actualCycleTime - theoreticalCycleTime) / theoreticalCycleTime) * 100;\n\n cycleEvent = {\n anomaly_type: \"slow-cycle\",\n severity: \"warning\",\n requires_ack: false,\n title: \"Slow Cycle Detected\",\n description: `Cycle took ${actualCycleTime.toFixed(1)}s (+${deltaPercent.toFixed(0)}% vs ${theoreticalCycleTime.toFixed(1)}s target)`,\n data: {\n actual_cycle_time: actualCycleTime,\n theoretical_cycle_time: theoreticalCycleTime,\n delta_percent: Math.round(deltaPercent),\n micro_threshold_multiplier: microMultiplier,\n macro_threshold_multiplier: macroMultiplier\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: currentCycleCount,\n tsMs: now\n };\n\n //node.warn(`[SLOW-CYCLE] ${actualCycleTime.toFixed(1)}s (expected ${theoreticalCycleTime}s)`);\n\n } /*else if (actualCycleTime >= macroThresholdSec) {\n cycleEvent = {\n anomaly_type: \"macrostop\",\n severity: \"critical\",\n requires_ack: true,\n title: \"Macrostop Detected\",\n description: `Cycle gap ${actualCycleTime.toFixed(1)}s (threshold: ${macroThresholdSec.toFixed(1)}s)`,\n data: {\n stoppage_duration_seconds: Math.round(actualCycleTime),\n theoretical_cycle_time: theoreticalCycleTime,\n micro_threshold_multiplier: microMultiplier,\n macro_threshold_multiplier: macroMultiplier\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: currentCycleCount,\n tsMs: now,\n status: \"resolved\" // Changed to resolved since cycle completed\n };\n\n node.warn(`[MACROSTOP] ${actualCycleTime.toFixed(1)}s gap detected`);\n }*/\n\n // FIXED: Always push cycle events (removed duplicate suppression)\n if (cycleEvent) {\n detectedAnomalies.push(cycleEvent);\n //node.warn(`[CYCLE EVENT] Added ${cycleEvent.anomaly_type} to detectedAnomalies`);\n }\n\n anomalyState.lastCycleEventCount = currentCycleCount;\n anomalyState.lastSlowCycleCount = currentCycleCount;\n }\n\n // No new cycle yet: start or escalate stoppage\n if (!hasNewCycle && lastCycleTime > 0) {\n // Start stoppage once when we cross micro threshold\n if (!anomalyState.activeStoppageEvent && timeSinceLastCycleSec >= microThresholdSec) {\n const isMacro = timeSinceLastCycleSec >= macroThresholdSec;\n const anomalyType = isMacro ? \"macrostop\" : \"microstop\";\n const severity = isMacro ? \"critical\" : \"warning\";\n\n const stoppageEvent = {\n alert_id: `${anomalyType}:${activeOrder.id}:${lastCycleTime}`,\n anomaly_type: anomalyType,\n severity,\n requires_ack: true,\n title: isMacro ? \"Macrostop In Progress\" : \"Microstop In Progress\",\n description: `No cycles for ${timeSinceLastCycleSec.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,\n data: {\n stoppage_duration_seconds: Math.round(timeSinceLastCycleSec),\n theoretical_cycle_time: theoreticalCycleTime,\n last_cycle_timestamp: lastCycleTime,\n micro_threshold_multiplier: microMultiplier,\n macro_threshold_multiplier: macroMultiplier\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: \"active\"\n };\n\n detectedAnomalies.push(stoppageEvent);\n anomalyState.activeStoppageEvent = stoppageEvent;\n anomalyState.lastStoppageUpdateMs = now;\n //node.warn(`[STOPPAGE START] ${anomalyType} started at ${timeSinceLastCycleSec.toFixed(0)}s`);\n }\n\n // Escalate micro -> macro once\n if (\n anomalyState.activeStoppageEvent &&\n anomalyState.activeStoppageEvent.anomaly_type === \"microstop\" &&\n timeSinceLastCycleSec >= macroThresholdSec\n ) {\n detectedAnomalies.push({\n ...anomalyState.activeStoppageEvent,\n status: \"resolved\",\n resolved_at: now,\n auto_resolved: true,\n requires_ack: false,\n data: {\n ...anomalyState.activeStoppageEvent.data,\n stoppage_duration_seconds: Math.round(timeSinceLastCycleSec)\n },\n tsMs: now\n });\n\n const macroEvent = {\n alert_id: `macrostop:${activeOrder.id}:${lastCycleTime}`,\n anomaly_type: \"macrostop\",\n severity: \"critical\",\n requires_ack: true,\n title: \"Macrostop In Progress\",\n description: `No cycles for ${timeSinceLastCycleSec.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,\n data: {\n stoppage_duration_seconds: Math.round(timeSinceLastCycleSec),\n theoretical_cycle_time: theoreticalCycleTime,\n last_cycle_timestamp: lastCycleTime,\n micro_threshold_multiplier: microMultiplier,\n macro_threshold_multiplier: macroMultiplier\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: \"active\"\n };\n\n detectedAnomalies.push(macroEvent);\n anomalyState.activeStoppageEvent = macroEvent;\n anomalyState.lastStoppageUpdateMs = now;\n //node.warn(`[ESCALATION] Microstop -> Macrostop at ${timeSinceLastCycleSec.toFixed(0)}s`);\n }\n\n // NEW: Send periodic updates for active macrostops\n if (\n anomalyState.activeStoppageEvent &&\n anomalyState.activeStoppageEvent.anomaly_type === \"macrostop\"\n ) {\n const timeSinceLastUpdate = now - (anomalyState.lastStoppageUpdateMs || 0);\n \n if (timeSinceLastUpdate >= updateIntervalMs) {\n const prev = anomalyState.activeStoppageEvent;\n\n // 1) AUTO-ACK the previous active macrostop alert\n const autoAck = {\n ...prev,\n status: \"resolved\",\n resolved_at: now,\n auto_resolved: true,\n requires_ack: false,\n title: \"Macrostop Updated\",\n description: \"Auto-acknowledged due to periodic refresh\",\n tsMs: now,\n is_auto_ack: true\n };\n\n // 2) Send a NEW active macrostop alert with updated duration + NEW alert_id\n const refreshed = {\n ...prev,\n alert_id: `macrostop:${activeOrder.id}:${now}`, // new instance id so UI treats it as new\n status: \"active\",\n requires_ack: true,\n title: \"Macrostop Ongoing\",\n description: `Machine still stopped for ${timeSinceLastCycleSec.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,\n data: {\n ...prev.data,\n stoppage_duration_seconds: Math.round(timeSinceLastCycleSec)\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n tsMs: now,\n is_update: true\n };\n\n detectedAnomalies.push(autoAck, refreshed);\n\n // IMPORTANT: set the active event to the refreshed one\n anomalyState.activeStoppageEvent = refreshed;\n anomalyState.lastStoppageUpdateMs = now;\n\n //node.warn(`[MACROSTOP REFRESH] Auto-acked previous, new duration: ${timeSinceLastCycleSec.toFixed(0)}s`);\n }\n }\n }\n}\n\n\n\n// ============================================================\n// TIER 2: OEE DROP DETECTION\n// Trigger: OEE falls below threshold\n// ============================================================\nconst currentOEE = Number(kpis.oee) || 0;\n\nif (currentOEE > 0) {\n const lowThreshold = OEE_THRESHOLD; // e.g. 90\n const recoveryThreshold = OEE_THRESHOLD + 2; // some hysteresis\n\n if (currentOEE < lowThreshold) {\n // Count consecutive low points\n anomalyState.oeeLowStreak = (anomalyState.oeeLowStreak || 0) + 1;\n\n const REQUIRED_STREAK = 3; // only alert after 3 bad readings\n\n // Only fire when we ENTER the bad zone\n if (!anomalyState.activeOeeDrop &&\n anomalyState.oeeLowStreak >= REQUIRED_STREAK) {\n\n let severity = 'warning';\n if (currentOEE < 75) severity = 'critical';\n\n detectedAnomalies.push({\n anomaly_type: 'oee-drop',\n severity,\n title: 'OEE Below Threshold',\n description: `OEE at ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`,\n data: {\n current_oee: currentOEE,\n threshold: OEE_THRESHOLD,\n delta: OEE_THRESHOLD - currentOEE\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: 'active'\n });\n\n anomalyState.activeOeeDrop = true;\n //node.warn(`[ANOMALY] OEE drop started at ${currentOEE.toFixed(1)}%`);\n }\n\n } else if (currentOEE >= recoveryThreshold) {\n // We are OUT of the bad zone\n anomalyState.oeeLowStreak = 0;\n\n if (anomalyState.activeOeeDrop) {\n detectedAnomalies.push({\n anomaly_type: 'oee-drop',\n severity: 'info',\n title: 'OEE Recovered',\n description: `OEE recovered to ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`,\n data: {\n current_oee: currentOEE,\n threshold: OEE_THRESHOLD\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: 'resolved'\n });\n\n anomalyState.activeOeeDrop = false;\n //node.warn(`[ANOMALY] OEE recovered to ${currentOEE.toFixed(1)}%`);\n }\n }\n}\n\n// Update OEE history for trend analysis\nanomalyState.oeeHistory.push({ tsMs: now, value: currentOEE });\nif (anomalyState.oeeHistory.length > HISTORY_WINDOW) {\n anomalyState.oeeHistory.shift(); // Keep only recent history\n}\n\n// ============================================================\n// TIER 2: QUALITY SPIKE DETECTION\n// Trigger: Sudden increase in scrap/defect rate\n// ============================================================\nconst totalParts = (cycle.goodParts || 0) + (cycle.scrapParts || 0);\nconst currentScrapRate =\n totalParts > 0 ? ((cycle.scrapParts || 0) / totalParts) * 100 : 0;\n\n// Keep history for trend\nanomalyState.qualityHistory.push({ tsMs: now, value: currentScrapRate });\nif (anomalyState.qualityHistory.length > HISTORY_WINDOW) {\n anomalyState.qualityHistory.shift();\n}\n\n// Only evaluate when we have enough data and enough volume in this cycle\nconst MIN_SAMPLES = 5;\nconst MIN_PARTS_THIS_CYCLE = 10;\n\nif (\n anomalyState.qualityHistory.length >= MIN_SAMPLES &&\n totalParts >= MIN_PARTS_THIS_CYCLE\n) {\n const recentHistory = anomalyState.qualityHistory.slice(0, -1); // exclude current\n const avgScrapRate =\n recentHistory.reduce((sum, p) => sum + p.value, 0) /\n recentHistory.length;\n\n const scrapRateIncrease = currentScrapRate - avgScrapRate;\n\n const SPIKE_DELTA = QUALITY_SPIKE_THRESHOLD || 5; // your existing config\n const MIN_SCRAP_RATE = 5; // ignore small scrap percentages\n const RECOVERY_MARGIN = 2; // how far back towards avg to consider \"recovered\"\n\n // ----- When scrap is HIGH / SPIKE ZONE -----\n if (\n currentScrapRate > MIN_SCRAP_RATE &&\n scrapRateIncrease > SPIKE_DELTA\n ) {\n // count how many consecutive \"bad\" cycles\n anomalyState.qualityHighStreak =\n (anomalyState.qualityHighStreak || 0) + 1;\n\n const REQUIRED_STREAK = 2; // require 2 consecutive bad cycles\n\n // fire ONLY when we ENTER the spike (not every cycle)\n if (\n !anomalyState.activeQualitySpike &&\n anomalyState.qualityHighStreak >= REQUIRED_STREAK\n ) {\n let severity = \"warning\";\n if (scrapRateIncrease > 10 || currentScrapRate > 15) {\n severity = \"critical\";\n }\n\n detectedAnomalies.push({\n anomaly_type: \"quality-spike\",\n severity,\n title: \"Quality Issue Detected\",\n description: `Scrap rate at ${currentScrapRate.toFixed(\n 1\n )}% (avg: ${avgScrapRate.toFixed(\n 1\n )}%, +${scrapRateIncrease.toFixed(1)}%)`,\n data: {\n current_scrap_rate: currentScrapRate,\n average_scrap_rate: avgScrapRate,\n increase: scrapRateIncrease,\n scrap_parts: cycle.scrapParts || 0,\n total_parts: totalParts\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: \"active\"\n });\n\n anomalyState.activeQualitySpike = true;\n /*node.warn(\n `[ANOMALY] Quality spike started: scrap ${currentScrapRate.toFixed(\n 1\n )}% (avg ${avgScrapRate.toFixed(1)}%)`\n );*/\n }\n } else {\n // ----- When scrap is NORMAL / RECOVERY -----\n anomalyState.qualityHighStreak = 0;\n\n // if we had an active spike, send a single \"resolved\" event\n if (\n anomalyState.activeQualitySpike &&\n currentScrapRate <= avgScrapRate + RECOVERY_MARGIN\n ) {\n detectedAnomalies.push({\n anomaly_type: \"quality-spike\",\n severity: \"info\",\n title: \"Quality Issue Resolved\",\n description: `Scrap rate back to ${currentScrapRate.toFixed(\n 1\n )}% (avg: ${avgScrapRate.toFixed(1)}%)`,\n data: {\n current_scrap_rate: currentScrapRate,\n average_scrap_rate: avgScrapRate\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: \"resolved\"\n });\n\n anomalyState.activeQualitySpike = false;\n /*node.warn(\n `[ANOMALY] Quality spike resolved: scrap ${currentScrapRate.toFixed(\n 1\n )}%`\n );*/\n }\n }\n}\n\n// ============================================================\n// TIER 2: PERFORMANCE DEGRADATION\n// Trigger: Consistent underperformance over time\n// ============================================================\nconst currentPerformance = Number(kpis.performance) || 0;\nanomalyState.performanceHistory.push({ tsMs: now, value: currentPerformance });\nif (anomalyState.performanceHistory.length > HISTORY_WINDOW) {\n anomalyState.performanceHistory.shift();\n}\n\n// Check for sustained poor performance (at least 10 data points)\nif (anomalyState.performanceHistory.length >= 10) {\n const recent10 = anomalyState.performanceHistory.slice(-10);\n const avgPerformance = recent10.reduce((sum, point) => sum + point.value, 0) / recent10.length;\n\n const PERF_LOW_THRESHOLD = PERFORMANCE_THRESHOLD; // 85%\n const PERF_RECOVERY_THRESHOLD = PERFORMANCE_THRESHOLD + 3; // 88% to recover\n\n // Check if we're in degraded state\n if (avgPerformance > 0 && avgPerformance < PERF_LOW_THRESHOLD) {\n // Count consecutive low readings\n anomalyState.performanceLowStreak = (anomalyState.performanceLowStreak || 0) + 1;\n\n const REQUIRED_STREAK = 3; // Need 3 consecutive low readings\n\n // Only fire ONCE when entering degraded state\n if (!anomalyState.activePerformanceDegradation &&\n anomalyState.performanceLowStreak >= REQUIRED_STREAK) {\n\n let severity = 'warning';\n if (avgPerformance < 75) {\n severity = 'critical';\n }\n\n detectedAnomalies.push({\n anomaly_type: 'performance-degradation',\n severity: severity,\n title: `Performance Degradation`,\n description: `Performance at ${avgPerformance.toFixed(1)}% (sustained over last 10 cycles)`,\n data: {\n average_performance: avgPerformance,\n current_performance: currentPerformance,\n threshold: PERF_LOW_THRESHOLD,\n sample_size: 10\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: 'active'\n });\n\n // Mark as active so we don't spam\n anomalyState.activePerformanceDegradation = true;\n //node.warn(`[ANOMALY] Performance degradation STARTED: ${avgPerformance.toFixed(1)}%`);\n }\n\n } else if (avgPerformance >= PERF_RECOVERY_THRESHOLD) {\n // Performance recovered\n anomalyState.performanceLowStreak = 0;\n\n // Only send recovery message if we were previously in degraded state\n if (anomalyState.activePerformanceDegradation) {\n detectedAnomalies.push({\n anomaly_type: 'performance-degradation',\n severity: 'info',\n title: 'Performance Recovered',\n description: `Performance recovered to ${avgPerformance.toFixed(1)}% (threshold: ${PERF_LOW_THRESHOLD}%)`,\n data: {\n average_performance: avgPerformance,\n current_performance: currentPerformance,\n threshold: PERF_LOW_THRESHOLD\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now,\n status: 'resolved'\n });\n\n anomalyState.activePerformanceDegradation = false;\n //node.warn(`[ANOMALY] Performance degradation RESOLVED: ${avgPerformance.toFixed(1)}%`);\n }\n }\n}\n\n// ============================================================\n// TIER 3: PREDICTIVE ALERTS (Trend Analysis)\n// Predict issues before they become critical\n// ============================================================\nif (anomalyState.oeeHistory.length >= 15) {\n // Simple linear trend analysis on OEE\n const recent15 = anomalyState.oeeHistory.slice(-15);\n const firstHalf = recent15.slice(0, 7);\n const secondHalf = recent15.slice(-7);\n\n const avgFirstHalf = firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length;\n const avgSecondHalf = secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length;\n\n const oeeTrend = avgSecondHalf - avgFirstHalf;\n\n // Predict if OEE is trending downward significantly\n if (oeeTrend < -5 && avgSecondHalf > OEE_THRESHOLD * 0.95 && avgSecondHalf < OEE_THRESHOLD * 1.05) {\n detectedAnomalies.push({\n anomaly_type: 'predictive-oee-decline',\n severity: 'info',\n title: `Declining OEE Trend Detected`,\n description: `OEE trending down ${Math.abs(oeeTrend).toFixed(1)}% over last 15 cycles. Current: ${avgSecondHalf.toFixed(1)}%`,\n data: {\n trend: oeeTrend,\n first_half_avg: avgFirstHalf,\n second_half_avg: avgSecondHalf,\n prediction: 'OEE may drop below threshold soon'\n },\n kpi_snapshot: {\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n },\n work_order_id: activeOrder.id,\n cycle_count: cycle.cycles || 0,\n tsMs: now\n });\n\n //node.warn(`[PREDICTIVE] OEE trending down: ${oeeTrend.toFixed(1)}%`);\n }\n}\n\n// Update last cycle time for next iteration\nanomalyState.lastCycleTime = lastCycleTime;\nglobal.set(\"anomalyState\", anomalyState);\n//anomaly.state = anomalyState;\n//global.set(\"anomaly\", anomaly);\n\n\n// ============================================================\n// OUTPUT\n// ============================================================\nconst normalizedAnomalies = detectedAnomalies.map(applyDefaultDowntimeReason);\n\nif (normalizedAnomalies.length > 0) {\n node.warn(`[ANOMALY DETECTOR] Detected ${normalizedAnomalies.length} anomaly/ies`);\n\n normalizedAnomalies.forEach((a, i) => {\n node.warn(` [${i + 1}] ${a.anomaly_type} - ${a.title} - ${a.status || 'N/A'}`);\n });\n\n msg.topic = \"anomaly-detected\";\n msg.payload = normalizedAnomalies;\n msg.originalMsg = msg.originalMsg || null;\n msg._anomaly_source = \"anomaly_detector\";\n return msg;\n}\nreturn null;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1350, + "y": 440, + "wires": [ + [ + "4a382caf6dadab4c", + "e5272e3e630666ce", + "a1e59ef54a91ee50", + "254622f455de3a4c" + ] + ] + }, + { + "id": "4a382caf6dadab4c", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Event Logger (Simplified)", + "func": "// ============================================================\n// EVENT LOGGER - SIMPLIFIED (INSERTS ONLY)\n// Every anomaly gets inserted as a new row\n// ============================================================\n\nconst anomalies = msg.payload || [];\nconst settings = global.get(\"settings\") || {};\nconst anomalyStateStore = global.get(\"anomaly\") || {};\nconst reasonsByIncident = anomalyStateStore.reasonsByIncident || {};\n\nif (!Array.isArray(anomalies) || anomalies.length === 0) {\n return null;\n}\n\n// SQL escape helper\nconst esc = (v) => {\n if (v === null || v === undefined) return 'NULL';\n return \"'\" + String(v).replace(/\\\\/g, '\\\\').replace(/'/g, \"''\") + \"'\";\n};\n\nconst dbInserts = [];\nconst activeAnomalies = [];\nconst softNotifications = []; \n\n\nanomalies.forEach(anomaly => {\n const tsMs = Number(anomaly.tsMs) || Date.now();\n const woId = anomaly.work_order_id || '';\n const aType = anomaly.anomaly_type || 'unknown';\n const sev = anomaly.severity || 'warning';\n const title = anomaly.title || '';\n const desc = anomaly.description || '';\n const dataJson = JSON.stringify(anomaly.data || {});\n const kpiJson = JSON.stringify(anomaly.kpi_snapshot || {});\n const cycle = Number(anomaly.cycle_count) || 0;\n const requiresAck = anomaly.requires_ack !== false;\n const incidentKey = anomaly.incidentKey || (anomaly.data && anomaly.data.last_cycle_timestamp\n ? [aType, woId, String(anomaly.data.last_cycle_timestamp)].join(\":\")\n : (anomaly.alert_id || null));\n const reason = incidentKey ? (reasonsByIncident[incidentKey] || null) : null;\n\n // Build INSERT query\n const insertQuery = \n \"INSERT INTO anomaly_events \" +\n \"(`event_timestamp`, `work_order_id`, `anomaly_type`, `severity`, `title`, `description`, \" +\n \"`data_json`, `kpi_snapshot_json`, `status`, `cycle_count`, `occurrence_count`, `last_occurrence`) VALUES (\" +\n tsMs + \", \" +\n esc(woId) + \", \" +\n esc(aType) + \", \" +\n esc(sev) + \", \" +\n esc(title) + \", \" +\n esc(desc) + \", \" +\n esc(dataJson) + \", \" +\n esc(kpiJson) + \", \" +\n \"'active', \" +\n cycle + \", \" +\n \"1, \" +\n tsMs + \")\";\n\n dbInserts.push({ topic: insertQuery, payload: [] });\n\n // Add to active list for UI\n if (requiresAck) {\n // Hard alerts that go to the panel + require acknowledgment\n activeAnomalies.push({\n event_id: null,\n tsMs: tsMs,\n work_order_id: woId,\n anomaly_type: aType,\n incidentKey: incidentKey || null,\n severity: sev,\n title: title,\n description: desc,\n status: 'active',\n reason: reason,\n kpi_snapshot: anomaly.kpi_snapshot || {}\n });\n\n node.warn(`[EVENT LOGGER] Inserting ${aType}: ${title} (requires ack)`);\n } else {\n // Soft alerts (e.g. slow-cycle) -> just show a transient popup\n softNotifications.push({\n tsMs: tsMs,\n anomaly_type: aType,\n severity: sev,\n title: title,\n description: desc,\n requires_ack: false\n });\n\n node.warn(`[EVENT LOGGER] Logging soft anomaly ${aType}: ${title}`);\n }\n});\n\n// UI update message\nconst uiMsg = {\n topic: \"anomaly-ui-update\",\n payload: {\n activeCount: activeAnomalies.length,\n activeAnomalies: activeAnomalies,\n updates: activeAnomalies.map(a => ({ status: 'new', anomaly: a })),\n softNotifications: softNotifications,\n reasonCatalog: settings.reasonCatalog || null\n }\n};\n\nreturn [dbInserts, uiMsg];\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1370, + "y": 400, + "wires": [ + [ + "1e8c2370ba268726", + "d116553e03c9a47c", + "4ea28603310c2aef" + ], + [ + "286df6ee52e241ca", + "fa4a2d371b22788a" + ] + ] + }, + { + "id": "d116553e03c9a47c", + "type": "split", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Split DB Inserts", + "splt": "\\n", + "spltType": "str", + "arraySplt": 1, + "arraySpltType": "len", + "stream": false, + "addname": "", + "x": 1940, + "y": 380, + "wires": [ + [ + "8dcfbe64c30024e6" + ] + ] + }, + { + "id": "2dffe9379e43af37", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "g": "40295dcf4b5e1779", + "mydb": "fc9634aabefee16b", + "name": "Anomaly Events DB", + "x": 1020, + "y": 60, + "wires": [ + [] + ] + }, + { + "id": "bcc7ea4f7444b199", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "Initialize OEE Threshold (90%)", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": 0.1, + "topic": "", + "payload": "90", + "payloadType": "num", + "x": 360, + "y": 940, + "wires": [ + [ + "407a6d0315f2812d" + ] + ] + }, + { + "id": "407a6d0315f2812d", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Set OEE Threshold Global", + "func": "// Initialize OEE alert threshold\nconst settings = global.get(\"settings\") || {};\nconst threshold = Number(msg.payload) || 90;\nsettings.oeeAlertThreshold = threshold;\nglobal.set(\"settings\", settings);\n\nnode.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`);\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 680, + "y": 940, + "wires": [ + [] + ] + }, + { + "id": "c323fd9c710bcded", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Save KPIs to Database", + "func": "// ============================================================\n// SAVE KPIs TO DATABASE - 1 SNAPSHOT PER CYCLE\n// ============================================================\n\nconst state = global.get(\"state\") || {};\nconst kpis = msg.kpis || {};\n\n// Rising edge guard: only save once per cycle\nconst saveFlag = state.saveKpis || 0;\nif (!saveFlag) {\n return null;\n}\nstate.saveKpis = 0;\nglobal.set(\"state\", state);\n\nconst dbInserts = [];\n\nconst activeOrder = state.activeWorkOrder || {};\nconst workorder_id = activeOrder.id;\nconst oee = Number(kpis.oee);\nconst performance = Number(kpis.performance);\nconst availability = Number(kpis.availability);\nconst quality = Number(kpis.quality);\nconst tsMs = Date.now();\n\nif (!workorder_id) {\n return null;\n}\n\nconst insertQuery =\n \"INSERT INTO kpi_snapshots \" +\n \"(work_order_id,oee_percent, performance_percent, availability_percent, quality_percent, timestamp) VALUES (\" +\n \"'\" + workorder_id + \"', \" +\n oee + \", \" +\n performance + \", \" +\n availability + \", \" +\n quality + \", \" +\n tsMs + \")\";\n\ndbInserts.push({ topic: insertQuery, payload: [] });\n\nreturn [dbInserts];\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1710, + "y": 720, + "wires": [ + [ + "8fe1661536e76e10" + ] + ] + }, + { + "id": "8fe1661536e76e10", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "mydb": "fc9634aabefee16b", + "name": "Save kpis to database", + "x": 1980, + "y": 700, + "wires": [ + [ + "1a14505b6501d2dc" + ] + ] + }, + { + "id": "429b86c710e30c33", + "type": "template", + "z": "8ccf34b55a2afcad", + "g": "8b73333804b30ee0", + "name": "Format query 1", + "field": "topic", + "fieldType": "msg", + "format": "handlebars", + "syntax": "mustache", + "template": "SELECT\n oee_percent,\n availability_percent,\n quality_percent,\n performance_percent,\n tsMs\nFROM (\n SELECT\n oee_percent,\n availability_percent,\n quality_percent,\n performance_percent,\n timestamp AS tsMs\n FROM kpi_snapshots\n ORDER BY timestamp DESC\n LIMIT 50\n) AS t\nORDER BY tsMs ASC;\n", + "output": "str", + "x": 1380, + "y": 80, + "wires": [ + [ + "2848a91e7a88cd0b" + ] + ] + }, + { + "id": "89d2d406e3cd96ee", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "8b73333804b30ee0", + "name": "Format Graph Data", + "func": "// Format Graph Data for KPI charts\n\n // Build labels and data arrays\n const labels = [];\n const oeeData = [];\n const availData = [];\n const perfData = [];\n const qualData = [];\n\n function bucketSeries(source, size) {\n const bucketSize = size || 5; // 3 points → 1 smoother point\n if (!Array.isArray(source) || source.length <= bucketSize) return source;\n\n const result = [];\n for (let i = 0; i < source.length; i += bucketSize) {\n const bucket = source.slice(i, i + bucketSize);\n const avgY = bucket.reduce((sum, p) => sum + Number(p.y || 0), 0) / bucket.length;\n const midPoint = bucket[Math.floor(bucket.length / 2)] || bucket[0];\n result.push({ x: midPoint.x, y: avgY });\n }\n return result;\n}\n\n \n msg.payload.forEach(row => {\n let x_value = new Date(row.tsMs); \n const dateObject = new Date(x_value);\n const year = dateObject.getFullYear();\n const month = dateObject.getMonth() + 1; // Months are 0-indexed\n const day = dateObject.getDate();\n const hours = dateObject.getHours();\n const minutes = dateObject.getMinutes();\n const seconds = dateObject.getSeconds();\n const formattedx_value = `${day.toString().padStart(2, '0')}-${month.toString().padStart(2, '0')} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n\n oeeData.push({ x: formattedx_value, y: row.oee_percent });\n availData.push({ x: formattedx_value, y: row.availability_percent });\n perfData.push({ x: formattedx_value, y: row.performance_percent });\n qualData.push({ x: formattedx_value, y: row.quality_percent });\n });\n\n const smoothOee = bucketSeries(oeeData, 5);\n const smoothAvail = bucketSeries(availData, 5);\n const smoothPerf = bucketSeries(perfData, 5);\n const smoothQual = bucketSeries(qualData, 5);\n\n msg.graphData = {\n labels: labels,\n datasets: [\n { label: 'OEE %', data: smoothOee },\n { label: 'Availability %', data: smoothAvail },\n { label: 'Performance %', data: smoothPerf },\n { label: 'Quality %', data: smoothQual }\n ]\n };\n\n //node.warn(`[GRAPH DATA] Formatted ${labels.length} KPI history points`);\n\n delete msg.topic;\n delete msg.payload;\n return msg;\n\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1830, + "y": 80, + "wires": [ + [ + "52501193c1677944" + ] + ] + }, + { + "id": "7a1de7abad9506ec", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "link out 11", + "mode": "link", + "links": [ + "154c7f1ad173419e" + ], + "x": 485, + "y": 360, + "wires": [] + }, + { + "id": "154c7f1ad173419e", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "1f2e45c551f40615", + "name": "link in 10", + "links": [ + "7a1de7abad9506ec" + ], + "x": 275, + "y": 860, + "wires": [ + [ + "851a0e837a94ed78" + ] + ] + }, + { + "id": "286df6ee52e241ca", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link out 12", + "mode": "link", + "links": [ + "184c513e8d1389dd" + ], + "x": 1685, + "y": 420, + "wires": [] + }, + { + "id": "184c513e8d1389dd", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "40295dcf4b5e1779", + "name": "link in 11", + "links": [ + "286df6ee52e241ca" + ], + "x": 265, + "y": 60, + "wires": [ + [ + "25e40f96b0876550" + ] + ] + }, + { + "id": "8dcfbe64c30024e6", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link out 13", + "mode": "link", + "links": [ + "d4ad56bfc7f333ba" + ], + "x": 2065, + "y": 380, + "wires": [] + }, + { + "id": "d4ad56bfc7f333ba", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "40295dcf4b5e1779", + "name": "link in 12", + "links": [ + "8dcfbe64c30024e6", + "1e8c2370ba268726" + ], + "x": 895, + "y": 80, + "wires": [ + [ + "2dffe9379e43af37" + ] + ] + }, + { + "id": "1e8c2370ba268726", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link out 14", + "mode": "link", + "links": [ + "d4ad56bfc7f333ba" + ], + "x": 2215, + "y": 420, + "wires": [] + }, + { + "id": "c72da0d5e173ccd5", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "c95c05b78d8464c5", + "name": "link out 15", + "mode": "link", + "links": [ + "9d321f6ae2618e66" + ], + "x": 1505, + "y": 200, + "wires": [] + }, + { + "id": "9d321f6ae2618e66", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "8b73333804b30ee0", + "name": "link in 13", + "links": [ + "c72da0d5e173ccd5" + ], + "x": 1235, + "y": 80, + "wires": [ + [ + "429b86c710e30c33" + ] + ] + }, + { + "id": "e5272e3e630666ce", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "link out 16", + "mode": "link", + "links": [], + "x": 1535, + "y": 440, + "wires": [] + }, + { + "id": "f1a89a5092479152", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "name": "Shift Config Handler", + "func": "// Shift Config Handler\nconst topic = msg.topic || \"\";\nconst config = global.get(\"config\") || {};\nconst readOnly = config.settingsReadOnly !== false;\nconst settings = global.get(\"settings\") || {};\n\nif (readOnly && (topic === \"saveShiftConfig\" || topic === \"saveThresholdConfig\" || topic === \"saveAllSettings\")) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Read-only\" });\n return null;\n}\n\n// Save shift config\nif (topic === \"saveShiftConfig\") {\n const config = msg.payload || {};\n settings.shifts = config.shifts || [];\n settings.shiftChangeCompensation = config.shiftChangeCompensation || 10;\n settings.lunchBreakMinutes = config.lunchBreakMinutes || 30;\n global.set(\"settings\", settings);\n\n node.status({ fill: \"green\", shape: \"dot\", text: `${config.shifts.length} sh ${config.shiftChangeCompensation} ch ${config.shiftChangeCompensation} lu` });\n return null;\n}\n\n// Save threshold config\nif (topic === \"saveThresholdConfig\") {\n const config = msg.payload || {};\n settings.thresholdMultiplier = config.thresholdMultiplier || 1.5;\n settings.macroStoppageMultiplier = config.macroStoppageMultiplier || 5;\n settings.oeeAlertThreshold = config.oeeAlertThreshold || 90;\n global.set(\"settings\", settings);\n\n node.status({ fill: \"green\", shape: \"dot\", text: `Threshold: ${config.thresholdMultiplier}x / ${config.macroStoppageMultiplier}x` });\n return null;\n}\n\n// Load shift config\nif (topic === \"getShiftConfig\") {\n msg.topic = \"shiftConfigData\";\n msg.payload = {\n shifts: settings.shifts || [{ start: \"08:00\", end: \"16:00\" }],\n shiftChangeCompensation: settings.shiftChangeCompensation || 10,\n lunchBreakMinutes: settings.lunchBreakMinutes || 30,\n thresholdMultiplier: settings.thresholdMultiplier || 1.5,\n macroStoppageMultiplier: settings.macroStoppageMultiplier || 5,\n oeeAlertThreshold: settings.oeeAlertThreshold || 90\n };\n return msg; // Send back to UI\n}\n\n// Save all settings at once\nif (topic === \"getShiftConfig\") {\n msg.topic = \"shiftConfigData\";\n msg.payload = {\n shifts: settings.shifts || [{ start: \"08:00\", end: \"16:00\" }],\n shiftChangeCompensation: settings.shiftChangeCompensation || 10,\n lunchBreakMinutes: settings.lunchBreakMinutes || 30,\n thresholdMultiplier: settings.thresholdMultiplier || 1.5,\n macroStoppageMultiplier: settings.macroStoppageMultiplier || 5,\n oeeAlertThreshold: settings.oeeAlertThreshold || 90\n };\n return msg; // Send back to UI\n}\n\nreturn null;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 400, + "y": 720, + "wires": [ + [ + "b45e6bfabde1f79c" + ] + ] + }, + { + "id": "b45e6bfabde1f79c", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "name": "link out 17", + "mode": "link", + "links": [ + "a51caffa7492c805" + ], + "x": 525, + "y": 720, + "wires": [] + }, + { + "id": "2d893a89211ae18f", + "type": "inject", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "KPI Tick", + "props": [ + { + "p": "payload" + } + ], + "repeat": "1", + "crontab": "", + "once": true, + "onceDelay": 0.1, + "topic": "", + "payload": "1", + "payloadType": "num", + "x": 1200, + "y": 600, + "wires": [ + [ + "ddf6d786b5a7e682" + ] + ] + }, + { + "id": "6f5af40a0e8ea8be", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 2", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 2360, + "y": 140, + "wires": [] + }, + { + "id": "9a2690258e151b66", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "function 1", + "func": "let last = context.get('last') || 0;\nlet current = Number(msg.payload);\nif (current !== last) {\n context.set('last', current);\n return msg;\n}\nreturn null;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2400, + "y": 240, + "wires": [ + [ + "6f5af40a0e8ea8be", + "ac25819b6b50ab90" + ] + ] + }, + { + "id": "af0fb4f5a75407ac", + "type": "switch", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "", + "property": "topic", + "propertyType": "msg", + "rules": [ + { + "t": "istype", + "v": "string", + "vt": "string" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 1530, + "y": 820, + "wires": [ + [ + "037e4825e2537ed2" + ] + ] + }, + { + "id": "1a14505b6501d2dc", + "type": "change", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Reset saveKpis flag", + "rules": [ + { + "t": "set", + "p": "state", + "pt": "global", + "to": "$merge([($globalContext(\"state\") ? $globalContext(\"state\") : {}), {\"saveKpis\": 0}])", + "tot": "jsonata" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 2200, + "y": 660, + "wires": [ + [] + ] + }, + { + "id": "e8efa837400e163b", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 4", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2140, + "y": 80, + "wires": [] + }, + { + "id": "873eb9e13455403e", + "type": "rpi-gpio in", + "z": "8ccf34b55a2afcad", + "d": true, + "name": "", + "pin": "17", + "intype": "up", + "debounce": "25", + "read": true, + "bcm": true, + "x": 2220, + "y": 200, + "wires": [ + [ + "9a2690258e151b66" + ] + ] + }, + { + "id": "8c52b16f9c4671d0", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Build Current State Snapshot", + "func": "// Build Current State Snapshot (outbox payload)\n\nconst config = global.get(\"config\") || {};\nconst machineId = config.machineId;\nconst now = Date.now();\nconst state = global.get(\"state\") || {};\nconst snapshot = state.lastState;\nconst settings = global.get(\"settings\") || {};\nconst lastMoldActive = Number(state.lastMoldActive ?? 0);\nconst moldActive = Number(\n snapshot?.activeWorkOrder?.cavities ??\n lastMoldActive ??\n snapshot?.cavities ??\n settings.moldActive ??\n global.get(\"moldActive\") ??\n 0\n);\nconst cavities = moldActive > 0 ? moldActive : null;\nmsg.tsMs = now;\n\n// This is the state you already have (from UI or from your state builder)\nconst s = msg.payload || {};\n\nmsg._mode = \"current-state\";\n\n// what you POST to cloud\nmsg.payload = {\n machineId: machineId,\n tsMs: now,\n activeWorkOrder: s.activeWorkOrder ?? null,\n cycle_count: s.cycleCount ?? null,\n good_parts: s.goodParts ?? null,\n scrap_parts: s.scrapParts ?? null,\n cavities,\n cycleTime: s.cycleTime ?? null,\n actualCycleTime: s.actualCycleTime ?? null,\n trackingEnabled: s.trackingEnabled ?? null,\n productionStarted: s.productionStarted ?? null,\n kpis: s.kpis ?? null,\n};\n\n// what goes into outbox (MUST MATCH what you intend to send)\nconst p = msg.payload || {};\n\nmsg.outbox = {\n type: \"kpi\",\n payload: {\n tsMs: p.tsMs,\n activeWorkOrder: p.activeWorkOrder,\n cycle_count: p.cycle_count,\n good_parts: p.good_parts,\n scrap_parts: p.scrap_parts,\n cavities: p.cavities,\n cycleTime: p.cycleTime,\n actualCycleTime: p.actualCycleTime,\n trackingEnabled: p.trackingEnabled,\n productionStarted: p.productionStarted,\n kpis: p.kpis,\n },\n};\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2420, + "y": 980, + "wires": [ + [ + "7cd3a5e80eb2b6e1" + ] + ] + }, + { + "id": "a1e59ef54a91ee50", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Build Event Outbox Payload", + "func": "// Build event outbox payload(s) — handle both single event and array (from Anomaly Detector).\n// Original single-event behavior preserved byte-for-byte for scrap-entry and\n// downtime-acknowledged paths. Array input emits one outbox message per anomaly.\n\nconst anomaly = global.get(\"anomaly\") || {};\n\nconst incoming = msg.payload;\nconst rawEvents = Array.isArray(incoming)\n ? incoming\n : (incoming && typeof incoming === \"object\" ? [incoming] : []);\nif (rawEvents.length === 0) return null;\n\nfor (const raw of rawEvents) {\n if (!raw || typeof raw !== \"object\") continue;\n\n const event = { ...raw };\n const tsMs = typeof event.tsMs === \"number\" ? event.tsMs : Date.now();\n\n const incidentKey =\n event.incidentKey ||\n event.incident_key ||\n (event.data && event.data.last_cycle_timestamp\n ? [event.anomaly_type || event.anomalyType || \"event\",\n event.work_order_id || event.workOrderId || \"\",\n String(event.data.last_cycle_timestamp)].join(\":\")\n : null);\n\n const reasonFromStore = incidentKey && anomaly.reasonsByIncident\n ? anomaly.reasonsByIncident[incidentKey]\n : null;\n if (!event.reason && reasonFromStore) event.reason = reasonFromStore;\n\n const anomalyType = event.anomaly_type || event.anomalyType || null;\n const isDowntimeType = anomalyType === \"microstop\" || anomalyType === \"macrostop\";\n if (!event.downtime) {\n event.downtime = isDowntimeType ? {\n incidentKey: incidentKey || null,\n anomalyType,\n durationSeconds: event.data && Number(event.data.stoppage_duration_seconds) || null\n } : null;\n }\n\n node.send({\n ...msg,\n tsMs,\n outbox: { type: \"event\", payload: { event } }\n });\n}\n\nreturn null;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2540, + "y": 780, + "wires": [ + [ + "4e12fb078508f92e" + ] + ] + }, + { + "id": "27df5e222b57f367", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "5", + "crontab": "", + "once": true, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 2290, + "y": 700, + "wires": [ + [ + "75380f6a82ade0da" + ] + ] + }, + { + "id": "75380f6a82ade0da", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Online HeartBeat", + "func": "// Heartbeat Producer (feeds Outbox Enqueue v1)\n\nconst config = global.get(\"config\") || {};\nconst machineId = msg.machineId || config.machineId;\nif (!machineId) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Heartbeat waiting for pairing\" });\n return null;\n}\nmsg.machineId = machineId;\n\n// Edge heartbeat = \"I am alive and running Node-RED\"\nconst status = \"ONLINE\";\n\n// pull these from globals if possible (set once at boot)\nconst ip = config.edgeIp || msg.ip || \"192.168.18.33\";\nconst fwVersion = config.fwVersion || \"raspi-nodered-1.0\";\nconst message = \"NR heartbeat\";\n\n// ---- DEDUPE / THROTTLE ----\n// Only enqueue if changed OR interval elapsed\nconst now = Date.now();\nmsg.tsMs = now;\nmsg.tsDevice = now; // epoch ms for API v1 (same instant as tsMs)\nconst intervalMs = Number(config.heartbeatIntervalMs || 15000);\n\n// Only include \"stable\" fields in signature; don't include timestamps\nconst signature = JSON.stringify({ status, ip, fwVersion });\n\nconst last = flow.get(\"hb_last\");\nif (last && last.signature === signature && (now - last.tsMs) < intervalMs) {\n return null; // skip enqueue (prevents spamming)\n}\nflow.set(\"hb_last\", { signature, tsMs: now });\n\n// This is what Outbox Enqueue v1 consumes\nmsg.outbox = {\n type: \"heartbeat\",\n payload: { status, message, ip, fwVersion },\n};\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2540, + "y": 640, + "wires": [ + [ + "0d19192a0f6a11dd" + ] + ] + }, + { + "id": "d685745ab029bddb", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Build Cycle Outbox Payload", + "func": "// Build cycle outbox payload (direct HTTP disabled)\n\nconst config = global.get(\"config\") || {};\nconst machineId = config.machineId;\n\nif (!msg.cycleRow) return null;\n\nmsg.tsMs = typeof msg.cycleRow.tsMs === \"number\" ? msg.cycleRow.tsMs : Date.now();\n\n// Deduplicate (extra safety)\nconst dedupeKey = `cc:${msg.cycleRow.cycle_count}`;\nconst lastKey = flow.get(\"lastCyclePostedKey\");\nif (lastKey === dedupeKey) return null;\nflow.set(\"lastCyclePostedKey\", dedupeKey);\n\n// For debugging visibility\nmsg._debug = {\n machineId: machineId,\n cycle_count: msg.cycleRow.cycle_count,\n actual_cycle_time: msg.cycleRow.actual_cycle_time,\n theoretical_cycle_time: msg.cycleRow.theoretical_cycle_time,\n};\n\nmsg.outbox = {\n type: \"cycle\",\n payload: { cycle: msg.cycleRow },\n};\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2540, + "y": 720, + "wires": [ + [ + "4e12fb078508f92e" + ] + ] + }, + { + "id": "1f4b73857950a541", + "type": "http request", + "z": "8ccf34b55a2afcad", + "d": true, + "name": "Legacy Direct Cycle HTTP (disabled)", + "method": "POST", + "ret": "txt", + "paytoqs": "ignore", + "url": "http://mis.maliountech.com.mx/api/ingest/cycle", + "tls": "", + "persist": false, + "proxy": "", + "insecureHTTPParser": false, + "authType": "", + "senderr": false, + "headers": [ + { + "keyType": "Content-Type", + "keyValue": "", + "valueType": "other", + "valueValue": "application/json" + }, + { + "keyType": "other", + "keyValue": "x-api-key", + "valueType": "other", + "valueValue": "e0113ab0688769179fce30a44d21e4fd0576747b0886e9a1" + } + ], + "x": 2770, + "y": 320, + "wires": [ + [ + "8a1b2c11abab88f9" + ] + ] + }, + { + "id": "8a1b2c11abab88f9", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 9", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2660, + "y": 180, + "wires": [] + }, + { + "id": "70999189874246f1", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "0", + "payload": "1", + "payloadType": "num", + "x": 2500, + "y": 360, + "wires": [ + [ + "9a2690258e151b66" + ] + ] + }, + { + "id": "4e12fb078508f92e", + "type": "subflow:c19582f27bf28841", + "z": "8ccf34b55a2afcad", + "name": "Outbox Enqueue v1", + "x": 2920, + "y": 780, + "wires": [ + [ + "eb1f811daf227ecc", + "eafd06a1e9e1ff30" + ] + ] + }, + { + "id": "4ed85f42c5f02ddf", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "Publisher tick", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "10", + "crontab": "", + "once": true, + "onceDelay": 0.5, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 2480, + "y": 920, + "wires": [ + [ + "eafd06a1e9e1ff30" + ] + ] + }, + { + "id": "94e5952704824302", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "mydb": "fc9634aabefee16b", + "name": "Fetch pending outbox", + "x": 3020, + "y": 880, + "wires": [ + [ + "4301be4fea74e008" + ] + ] + }, + { + "id": "eafd06a1e9e1ff30", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Select pending batch", + "func": "// Assume data\n\n\n// Set the SQL query in msg.topic using named parameters\nmsg.topic = `SELECT id, machine_id, msg_type, endpoint, schema_version, seq, ts_device_ms,\n payload_json, attempts, next_attempt_at\nFROM outbox_messages\nWHERE status='pending'\n AND (next_attempt_at IS NULL OR next_attempt_at <= NOW())\nORDER BY id ASC\nLIMIT 25;\n`\n// Set the values in msg.payload as an object\n\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2800, + "y": 880, + "wires": [ + [ + "94e5952704824302", + "a591bb8f1f62f230" + ] + ] + }, + { + "id": "4301be4fea74e008", + "type": "switch", + "z": "8ccf34b55a2afcad", + "name": "Has rows?", + "property": "payload", + "propertyType": "msg", + "rules": [ + { + "t": "eq", + "v": "", + "vt": "str" + }, + { + "t": "neq", + "v": "", + "vt": "str" + } + ], + "checkall": "true", + "repair": false, + "outputs": 2, + "x": 3230, + "y": 880, + "wires": [ + [], + [ + "8775ba662582c14f" + ] + ] + }, + { + "id": "8775ba662582c14f", + "type": "split", + "z": "8ccf34b55a2afcad", + "name": "Split rows", + "splt": "\\n", + "spltType": "str", + "arraySplt": 1, + "arraySpltType": "len", + "stream": false, + "addname": "", + "property": "payload", + "x": 3420, + "y": 880, + "wires": [ + [ + "8962b5831f39dcf2" + ] + ] + }, + { + "id": "8962b5831f39dcf2", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Build HTTP request", + "func": "const row = msg.payload; // row from DB\n\nconst config = global.get(\"config\") || {};\nconst base = config.cloudBaseUrl;\nconst apiKey = config.apiKey;\n\nconst machineId = config.machineId;\n\nif (machineId && row && row.machine_id && String(row.machine_id) !== String(machineId)) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Stale row (machine mismatch)\" });\n msg.topic = `\nUPDATE outbox_messages\nSET status='failed',\n last_http_status=?,\n last_error=?,\n next_attempt_at=NULL\nWHERE id=?;\n`.trim();\n msg.payload = [409, \"stale_machine\", Number(row.id)];\n return [null, msg];\n}\n\n\n\nif (!base || !apiKey) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Publisher waiting for pairing\" });\n return null;\n}\n\nconst baseUrl = String(base).replace(/\\/+$/, \"\");\nconst endpoint = String(row.endpoint || \"\");\nif (!endpoint.startsWith(\"/\")) throw new Error(\"Publisher: bad endpoint on row \" + row.id);\n\nmsg._row = row;\n\nmsg.method = row.method || \"POST\";\nmsg.url = baseUrl + endpoint;\n\nmsg.headers = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": apiKey,\n};\n\n// payload_json may be string (most common) or object depending on mysql node\nlet payload = row.payload_json;\nif (typeof payload === \"string\") {\n try { payload = JSON.parse(payload); } catch (e) { }\n}\nmsg.payload = payload;\n\nreturn msg;\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3610, + "y": 880, + "wires": [ + [ + "6e92e862a70834f1", + "30d6d75548f0bc28" + ], + [ + "9166011d8123adef" + ] + ] + }, + { + "id": "3c747f2b199404f6", + "type": "http request", + "z": "8ccf34b55a2afcad", + "name": "Send outbox HTTP", + "method": "use", + "ret": "txt", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "insecureHTTPParser": false, + "authType": "", + "senderr": false, + "headers": [], + "x": 3710, + "y": 680, + "wires": [ + [ + "004d80a4f572b7dc" + ] + ] + }, + { + "id": "004d80a4f572b7dc", + "type": "switch", + "z": "8ccf34b55a2afcad", + "name": "HTTP 200?", + "property": "statusCode", + "propertyType": "msg", + "rules": [ + { + "t": "eq", + "v": "200", + "vt": "str" + }, + { + "t": "neq", + "v": "200", + "vt": "str" + } + ], + "checkall": "true", + "repair": false, + "outputs": 2, + "x": 3850, + "y": 880, + "wires": [ + [ + "9aa8958ed50e1a38" + ], + [ + "96e08462edb58122" + ] + ] + }, + { + "id": "9aa8958ed50e1a38", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Mark Sent", + "func": "// Build Sent Update (use this right after HTTP request success branch)\nconst row = msg._row; // <-- IMPORTANT\nconst status = Number(msg.statusCode ?? 0);\n\nmsg.topic = `\n UPDATE outbox_messages\n SET status='sent',\n sent_at=NOW(),\n last_http_status=?,\n last_error=NULL\n WHERE id=?;\n`.trim();\n\nmsg.payload = [status, Number(row.id)];\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 4020, + "y": 860, + "wires": [ + [ + "9166011d8123adef" + ] + ] + }, + { + "id": "96e08462edb58122", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Retry", + "func": "const row = msg._row;\nconst attempts = Number(row.attempts || 0) + 1;\n\nconst retryConfig = {\n shortDelaySec: 5,\n mediumDelaySec: 30,\n longDelaySec: 180,\n mediumAfter: 5,\n longAfter: 20,\n errorMaxLen: 450,\n};\n\nconst status = Number(msg.statusCode || 0);\nlet err = \"\";\nif (msg.payload && typeof msg.payload === \"object\") {\n err = JSON.stringify(msg.payload).slice(0, retryConfig.errorMaxLen);\n} else if (msg.payload != null) {\n err = String(msg.payload).slice(0, retryConfig.errorMaxLen);\n} else {\n err = \"request_failed\";\n}\n\nif (status === 401 || status === 403) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Unauthorized - re-pair\" });\n msg.topic = `\nUPDATE outbox_messages\nSET status='failed',\n last_http_status=?,\n last_error=?,\n next_attempt_at=NULL\nWHERE id=?;\n`.trim();\n msg.payload = [ status || null, err, Number(row.id) ];\n return msg;\n}\n\n// Backoff policy\nlet delaySec = retryConfig.shortDelaySec;\nif (attempts <= retryConfig.mediumAfter) delaySec = retryConfig.shortDelaySec;\nelse if (attempts <= retryConfig.longAfter) delaySec = retryConfig.mediumDelaySec;\nelse delaySec = retryConfig.longDelaySec;\n\nmsg.topic = `\nUPDATE outbox_messages\nSET attempts=?,\n next_attempt_at=DATE_ADD(NOW(), INTERVAL ? SECOND),\n last_http_status=?,\n last_error=?\nWHERE id=?;\n`.trim();\n\nmsg.payload = [ attempts, delaySec, status || null, err, Number(row.id) ];\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 4010, + "y": 900, + "wires": [ + [ + "9166011d8123adef" + ] + ] + }, + { + "id": "9166011d8123adef", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "mydb": "fc9634aabefee16b", + "name": "Update outbox status", + "x": 4450, + "y": 900, + "wires": [ + [ + "d1d1cbe0ccc7236b" + ] + ] + }, + { + "id": "c48444f30fc71bae", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "State Accumulator global", + "func": "const config = global.get(\"config\") || {};\nconst state = global.get(\"state\") || {};\nconst settings = global.get(\"settings\") || {};\nconst machineId = config.machineId; // set this once at boot (see below)\nif (!machineId) { node.warn(\"lastState: missing config.machineId\"); return null; }\nconst active = state.activeWorkOrder || null;\nconst trackingEnabled = !!state.trackingEnabled;\nconst productionStarted = !!state.productionStarted;\nconst kpis = msg.kpis || state.currentKPIs || { oee: 0, availability: 0, performance: 0, quality: 0 };\nconst moldByWorkOrder = state.moldByWorkOrder || {};\nconst moldActive = Number(\n active?.cavities ??\n moldByWorkOrder[active?.id]?.active ??\n state.lastMoldActive ??\n settings.moldActive ??\n global.get(\"moldActive\") ??\n 0\n);\nconst cavities = moldActive > 0 ? moldActive : null;\n\nconst lastState = {\n machineId,\n activeWorkOrder: active,\n cycleCount: Number(state.cycleCount ?? 0),\n goodParts: Number(active?.goodParts ?? 0),\n scrapParts: Number(active?.scrapParts ?? 0),\n cavities,\n cycleTime: Number(active?.cycleTime ?? state.lastCycleTime ?? 0),\n actualCycleTime: Number(state.lastActualCycleTime ?? 0),\n trackingEnabled,\n productionStarted,\n kpis,\n tsMs: Date.now(),\n};\n\nstate.lastState = lastState;\nglobal.set(\"state\", state);\nmsg.lastState = lastState;\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1690, + "y": 760, + "wires": [ + [] + ] + }, + { + "id": "785db0c7cb865cc2", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "KPI state getter", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "60", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 2210, + "y": 1020, + "wires": [ + [ + "0ce8e4e0ba61ec80" + ] + ] + }, + { + "id": "0ce8e4e0ba61ec80", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Build KPI Outbox from lastState", + "func": "// Build KPI Outbox from lastState\n\nconst config = global.get(\"config\") || {};\nconst state = global.get(\"state\") || {};\nconst snapshot = state.lastState;\nconst settings = global.get(\"settings\") || {};\nconst moldByWorkOrder = state.moldByWorkOrder || {};\nconst moldActive = Number(settings.moldActive ?? global.get(\"moldActive\") ?? snapshot?.activeWorkOrder?.cavities ?? 0);\n//const cavities = moldActive > 0 ? moldActive : snapshot?.cavities ?? null;\n\nif (!snapshot) return null;\n\nconst machineId = snapshot.machineId || config.machineId;\nif (!machineId) return null;\n\nmsg.machineId = machineId;\n\nconst activeWorkOrder = snapshot?.activeWorkOrder ? { ...snapshot.activeWorkOrder } : null;\nif (activeWorkOrder?.lastUpdateIso && typeof activeWorkOrder.lastUpdateIso !== \"string\") {\n delete activeWorkOrder.lastUpdateIso;\n}\n\nconst cavitiesRaw = Number(\n activeWorkOrder?.cavities ??\n moldByWorkOrder[activeWorkOrder?.id]?.active ??\n snapshot?.cavities ??\n state.lastMoldActive ??\n settings.moldActive ??\n global.get(\"moldActive\") ??\n 0\n);\nconst cavities = cavitiesRaw > 0 ? cavitiesRaw : null;\n\n\n\nmsg.outbox = {\n type: \"kpi\",\n payload: {\n tsMs: snapshot.tsMs,\n activeWorkOrder,\n cycle_count: snapshot.cycleCount,\n good_parts: snapshot.goodParts,\n scrap_parts: snapshot.scrapParts,\n cavities,\n cycleTime: snapshot.cycleTime,\n actualCycleTime: snapshot.actualCycleTime,\n trackingEnabled: snapshot.trackingEnabled,\n productionStarted: snapshot.productionStarted,\n kpis: snapshot.kpis,\n },\n};\n\nmsg.tsMs = typeof snapshot.tsMs === \"number\" ? snapshot.tsMs : Date.now();\n\n// keep timestamp so you can see it ticking\nmsg.payload = msg.tsMs;\nmsg.topic = \"\";\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2450, + "y": 1020, + "wires": [ + [ + "818de86c22724aa5" + ] + ] + }, + { + "id": "eb1f811daf227ecc", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 1", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3190, + "y": 1120, + "wires": [] + }, + { + "id": "d1d1cbe0ccc7236b", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 3", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4540, + "y": 1100, + "wires": [] + }, + { + "id": "7cd3a5e80eb2b6e1", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 5", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2590, + "y": 1220, + "wires": [] + }, + { + "id": "818de86c22724aa5", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 6", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3030, + "y": 1200, + "wires": [] + }, + { + "id": "6e92e862a70834f1", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 7", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3400, + "y": 1080, + "wires": [] + }, + { + "id": "a591bb8f1f62f230", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 8", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3050, + "y": 1020, + "wires": [] + }, + { + "id": "917065027876ecd6", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 10", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2010, + "y": 1080, + "wires": [] + }, + { + "id": "5c86dbb1f8044c9b", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 11", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2000, + "y": 1140, + "wires": [] + }, + { + "id": "03fa4c64efd95c8a", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "Init config inject", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 3250, + "y": 220, + "wires": [ + [ + "a9fa224f48b83f71" + ] + ] + }, + { + "id": "c904233ea449e9e0", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "name": "Pair Machine Request", + "func": "const topic = msg.topic || \"\";\nif (topic != \"pairMachine\") {\n return null;\n}\n\nconst raw = (msg.payload && (msg.payload.code || msg.payload.pairingCode || msg.payload)) || \"\";\nconst code = String(raw).trim().toUpperCase().replace(/[^A-Z0-9]/g, \"\");\n\nif (code.length != 5) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Bad code\" });\n return [null, { topic: \"pairMachineResult\", payload: { ok: false, error: \"Codigo invalido.\" } }];\n}\n\nconst config = global.get(\"config\") || {};\nconst baseUrl = String(config.cloudBaseUrl || \"https://mis.maliountech.com.mx\").replace(/\\/+$/, \"\");\n\nmsg.method = \"POST\";\nmsg.url = baseUrl + \"/api/machines/pair\";\nmsg.headers = { \"Content-Type\": \"application/json\" };\nmsg.payload = { code: code };\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: \"Pairing...\" });\nreturn [msg, null];\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 430, + "y": 780, + "wires": [ + [ + "053e4c241fa29315" + ], + [ + "b45e6bfabde1f79c" + ] + ] + }, + { + "id": "053e4c241fa29315", + "type": "http request", + "z": "8ccf34b55a2afcad", + "name": "Pair Machine HTTP", + "method": "use", + "ret": "obj", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "insecureHTTPParser": false, + "authType": "", + "senderr": false, + "headers": [], + "x": 640, + "y": 780, + "wires": [ + [ + "60056bf2030382bc" + ] + ] + }, + { + "id": "60056bf2030382bc", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "c04e913f614d9036", + "name": "Pair Machine Response", + "func": "let res = msg.payload;\nif (typeof res == \"string\") {\n try { res = JSON.parse(res); } catch (e) { res = { error: res }; }\n}\nres = res || {};\n\nconst ok = res.ok === true || res.success === true;\nconst cfg = res.config || res;\n\nif (ok && cfg && cfg.machineId && cfg.apiKey) {\n const current = global.get(\"config\") || {};\n current.cloudBaseUrl = cfg.cloudBaseUrl || current.cloudBaseUrl;\n current.machineId = cfg.machineId;\n current.apiKey = cfg.apiKey;\n current.orgId = cfg.orgId || current.orgId;\n global.set(\"config\", current);\n\n node.status({ fill: \"green\", shape: \"dot\", text: \"Paired\" });\n msg.topic = \"pairMachineResult\";\n msg.payload = { ok: true, machineId: current.machineId };\n return msg;\n}\n\nconst error = (res && (res.error || res.message)) || (msg.statusCode ? (\"HTTP \" + msg.statusCode) : \"Pairing failed\");\nnode.status({ fill: \"red\", shape: \"ring\", text: \"Pair failed\" });\nmsg.topic = \"pairMachineResult\";\nmsg.payload = { ok: false, error: error };\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 860, + "y": 780, + "wires": [ + [ + "b45e6bfabde1f79c", + "34fe41a1d67fb9cd" + ] + ] + }, + { + "id": "5eeeac992826fa3b", + "type": "link in", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "link into settings", + "links": [], + "x": 255, + "y": 440, + "wires": [ + [ + "372edd599e1b2fdd" + ] + ] + }, + { + "id": "3dd6baf608946fde", + "type": "switch", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "Wifi switch", + "property": "topic", + "propertyType": "msg", + "rules": [ + { + "t": "eq", + "v": "wifi:scan", + "vt": "str" + }, + { + "t": "eq", + "v": "wifi:status", + "vt": "str" + }, + { + "t": "eq", + "v": "wifi:apply", + "vt": "str" + } + ], + "checkall": "true", + "repair": false, + "outputs": 3, + "x": 700, + "y": 460, + "wires": [ + [ + "799fcf989e49f86a" + ], + [ + "5a1b8c2451faa879" + ], + [ + "d7603f458aa33b0d" + ] + ] + }, + { + "id": "5a1b8c2451faa879", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "wifi:status from settings", + "mode": "link", + "links": [], + "x": 815, + "y": 460, + "wires": [] + }, + { + "id": "799fcf989e49f86a", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "wifi:scan from settings", + "mode": "link", + "links": [], + "x": 815, + "y": 420, + "wires": [] + }, + { + "id": "d7603f458aa33b0d", + "type": "link out", + "z": "8ccf34b55a2afcad", + "g": "472f828e204736a6", + "name": "wifi:apply from settings", + "mode": "link", + "links": [], + "x": 815, + "y": 500, + "wires": [] + }, + { + "id": "c238344865721014", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Parse settings update", + "func": "let payload = msg.payload;\n\nif (Buffer.isBuffer(payload)) {\n payload = payload.toString(\"utf8\");\n}\n\nif (typeof payload === \"string\") {\n try {\n payload = JSON.parse(payload);\n } catch (err) {\n payload = {};\n }\n}\n\nif (!payload || typeof payload !== \"object\") {\n payload = {};\n}\n\nconst topic = String(msg.topic || \"\");\nconst parts = topic.split(\"/\");\n\nfunction pickPart(label) {\n const idx = parts.indexOf(label);\n if (idx === -1) return null;\n return parts[idx + 1] || null;\n}\n\nconst orgId = payload.orgId || pickPart(\"org\");\nconst machineId = payload.machineId || pickPart(\"machines\");\n\nif (orgId) payload.orgId = orgId;\nif (machineId) payload.machineId = machineId;\n\nmsg.payload = payload;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 480, + "y": 920, + "wires": [ + [ + "ce21bcd6fddd75ef" + ] + ] + }, + { + "id": "ce21bcd6fddd75ef", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Fetch settings from Control Tower", + "func": "const config = global.get(\"config\") || {};\nconst base = config.cloudBaseUrl;\nconst apiKey = config.apiKey;\nconst machineId = config.machineId;\n\nif (!base || !apiKey || !machineId) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Settings fetch waiting for pairing\" });\n return null;\n}\n\nconst update = msg.payload || {};\nconst forced = msg.topic === \"settingsRefresh\" || msg.forceRefresh === true;\n\nif (!forced) {\n if (update.machineId && update.machineId !== machineId) {\n return null;\n }\n if (config.orgId && update.orgId && update.orgId !== config.orgId) {\n return null;\n }\n}\n\nconst baseUrl = String(base).replace(/\\/+$/, \"\");\nmsg.method = \"GET\";\nmsg.url = baseUrl + \"/api/settings/machines/\" + machineId;\nmsg.headers = {\n \"x-api-key\": apiKey,\n \"Accept\": \"application/json\"\n};\n\nmsg._settingsUpdate = {\n orgId: update.orgId || config.orgId,\n machineId: update.machineId || machineId,\n version: update.version\n};\nmsg._settingsRequestedAt = Date.now();\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 760, + "y": 920, + "wires": [ + [ + "59c3cac2af12053e" + ] + ] + }, + { + "id": "59c3cac2af12053e", + "type": "http request", + "z": "8ccf34b55a2afcad", + "name": "Fetch settings HTTP", + "method": "use", + "ret": "obj", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "insecureHTTPParser": false, + "authType": "", + "senderr": false, + "headers": [], + "x": 1020, + "y": 920, + "wires": [ + [ + "cc02262fda7af805" + ] + ] + }, + { + "id": "cc02262fda7af805", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Apply settings + update UI", + "func": "let payload = msg.payload;\n\nif (Buffer.isBuffer(payload)) {\n payload = payload.toString(\"utf8\");\n}\n\nif (typeof payload === \"string\") {\n try {\n payload = JSON.parse(payload);\n } catch (err) {\n payload = {};\n }\n}\n\nif (!payload || payload.ok === false) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Settings fetch failed\" });\n return [null, null];\n}\n\nconst effective = payload.effectiveSettings || payload.settings || payload;\nif (!effective || typeof effective !== \"object\") {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Settings payload missing\" });\n return [null, null];\n}\n\nconst defaults = effective.defaults || {};\nconst shiftSchedule = effective.shiftSchedule || {};\nconst thresholds = effective.thresholds || {};\nconst incomingCatalog = effective.reasonCatalog || null;\n\nconst settings = global.get(\"settings\") || {};\n\nconst nextMoldTotal = Number(defaults.moldTotal ?? settings.moldTotal ?? 0);\nconst nextMoldActive = Number(defaults.moldActive ?? settings.moldActive ?? 0);\n\nsettings.moldTotal = nextMoldTotal;\nsettings.moldActive = nextMoldActive;\n\nif (Array.isArray(shiftSchedule.shifts) && shiftSchedule.shifts.length) {\n settings.shifts = shiftSchedule.shifts.map((s) => ({\n start: s.start,\n end: s.end\n }));\n} else if (!Array.isArray(settings.shifts)) {\n settings.shifts = [{ start: \"08:00\", end: \"16:00\" }];\n}\n\nsettings.shiftChangeCompensation = Number(\n shiftSchedule.shiftChangeCompensationMin ?? settings.shiftChangeCompensation ?? 10\n);\nsettings.lunchBreakMinutes = Number(\n shiftSchedule.lunchBreakMin ?? settings.lunchBreakMinutes ?? 30\n);\n\nsettings.thresholdMultiplier = Number(\n thresholds.stoppageMultiplier ?? settings.thresholdMultiplier ?? 1.5\n);\nsettings.macroStoppageMultiplier = Number(\n thresholds.macroStoppageMultiplier ?? settings.macroStoppageMultiplier ?? 5\n);\nsettings.oeeAlertThreshold = Number(\n thresholds.oeeAlertThresholdPct ?? settings.oeeAlertThreshold ?? 90\n);\n\nconst normalizeCatalogItems = (list, fallbackLabelPrefix) => {\n if (!Array.isArray(list)) return [];\n return list\n .map((c, idx) => {\n const categoryId = String(c.id || c.categoryId || (\"cat_\" + idx));\n const categoryLabel = String(c.label || c.categoryLabel || (fallbackLabelPrefix + \" \" + (idx + 1)));\n const detailsRaw = Array.isArray(c.children) ? c.children : (Array.isArray(c.details) ? c.details : []);\n const details = detailsRaw.map((d, jdx) => ({\n id: String(d.id || d.detailId || (categoryId + \"_d\" + jdx)),\n label: String(d.label || d.detailLabel || (\"Detalle \" + (jdx + 1)))\n }));\n return {\n id: categoryId,\n label: categoryLabel,\n children: details\n };\n })\n .filter((c) => c.label && c.children.length > 0);\n};\n\nconst currentCatalog = settings.reasonCatalog || {};\nconst nextCatalogVersion =\n Number(\n (incomingCatalog && incomingCatalog.version) ??\n effective.reasonCatalogVersion ??\n currentCatalog.version ??\n 1\n ) || 1;\n\nconst hasIncomingCatalog = !!(incomingCatalog && (Array.isArray(incomingCatalog.downtime) || Array.isArray(incomingCatalog.scrap)));\nconst normalizedIncoming = hasIncomingCatalog ? {\n version: nextCatalogVersion,\n downtime: normalizeCatalogItems(incomingCatalog.downtime || [], \"Paro\"),\n scrap: normalizeCatalogItems(incomingCatalog.scrap || [], \"Scrap\")\n} : null;\n\nconst fallbackCatalog = {\n version: Number(currentCatalog.version || nextCatalogVersion || 1),\n downtime: normalizeCatalogItems(currentCatalog.downtime || [], \"Paro\"),\n scrap: normalizeCatalogItems(currentCatalog.scrap || [], \"Scrap\")\n};\n\nsettings.reasonCatalog = normalizedIncoming || fallbackCatalog;\nsettings.reasonCatalog.version = Number(settings.reasonCatalog.version || 1);\n\n\nif (effective.version !== undefined) {\n settings.version = Number(effective.version);\n}\n\nglobal.set(\"settings\", settings);\ntry {\n global.set(\"settings\", settings, \"file\");\n} catch (err) {\n // ignore if file store is not configured\n}\nglobal.set(\"moldActive\", settings.moldActive);\nglobal.set(\"moldTotal\", settings.moldTotal);\n\nconst config = global.get(\"config\") || {};\nif (effective.orgId && !config.orgId) {\n config.orgId = effective.orgId;\n global.set(\"config\", config);\n}\n\nnode.status({ fill: \"green\", shape: \"dot\", text: \"Settings synced\" });\n\nconst uiConfigMsg = {\n topic: \"shiftConfigData\",\n payload: {\n shifts: settings.shifts || [],\n shiftChangeCompensation: settings.shiftChangeCompensation || 10,\n lunchBreakMinutes: settings.lunchBreakMinutes || 30,\n thresholdMultiplier: settings.thresholdMultiplier || 1.5,\n macroStoppageMultiplier: settings.macroStoppageMultiplier || 5,\n oeeAlertThreshold: settings.oeeAlertThreshold || 90\n }\n};\n\n\nconst uiMoldMsg = {\n topic: \"moldPresetSelected\",\n payload: {\n total: settings.moldTotal || 0,\n active: settings.moldActive || 0\n }\n};\n\nconst readOnly = config.settingsReadOnly !== false;\nconst uiReadOnlyMsg = { topic: \"settingsReadOnly\", payload: readOnly };\nconst uiReasonCatalogMsg = {\n topic: \"reasonCatalogData\",\n payload: settings.reasonCatalog\n};\n\nnode.send([uiConfigMsg, null]);\nnode.send([uiMoldMsg, null]);\nnode.send([uiReadOnlyMsg, null]);\nnode.send([uiReasonCatalogMsg, null]);\n\nconst update = msg._settingsUpdate || {};\nconst orgId = update.orgId || config.orgId || effective.orgId;\nconst machineId = update.machineId || config.machineId;\nconst version = Number(effective.version ?? update.version ?? 0);\n\nif (!orgId || !machineId) {\n return [null, null];\n}\n\nconst prefix = String(config.mqttTopicPrefix || \"mis\").replace(/\\/+$/, \"\");\nconst ackTopic = prefix + \"/org/\" + orgId + \"/machines/\" + machineId + \"/settings/ack\";\n\nconst ackMsg = {\n topic: ackTopic,\n payload: JSON.stringify({\n type: \"settings_ack\",\n orgId,\n machineId,\n version,\n source: \"node-red\",\n ts: new Date().toISOString()\n })\n};\n\nreturn [null, ackMsg];\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1280, + "y": 920, + "wires": [ + [ + "b45e6bfabde1f79c", + "87dc48bbe753ab8d", + "25e40f96b0876550" + ], + [] + ] + }, + { + "id": "d063639e9402cb47", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "Refresh settings (poll)", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "600", + "crontab": "", + "once": true, + "onceDelay": 1, + "topic": "settingsRefresh", + "payload": "", + "payloadType": "date", + "x": 240, + "y": 980, + "wires": [ + [ + "ce21bcd6fddd75ef" + ] + ] + }, + { + "id": "8abddc40064f1f64", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Fetch work orders from Control Tower", + "func": "const config = global.get(\"config\") || {};\nconst base = config.cloudBaseUrl;\nconst apiKey = config.apiKey;\nconst machineId = config.machineId;\n\nif (!base || !apiKey || !machineId) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Work orders fetch waiting for pairing\" });\n return null;\n}\n\nconst update = msg.payload || {};\nif (update.machineId && update.machineId !== machineId) {\n return null;\n}\nif (config.orgId && update.orgId && update.orgId !== config.orgId) {\n return null;\n}\n\nconst baseUrl = String(base).replace(/\\/+$/, \"\");\nmsg.method = \"GET\";\nmsg.url = baseUrl + \"/api/work-orders/machines/\" + machineId;\nmsg.headers = {\n \"x-api-key\": apiKey,\n \"Accept\": \"application/json\"\n};\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1770, + "y": 320, + "wires": [ + [ + "16f0e85a7bc88164" + ] + ] + }, + { + "id": "16f0e85a7bc88164", + "type": "http request", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Fetch work orders HTTP", + "method": "use", + "ret": "obj", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "insecureHTTPParser": false, + "authType": "", + "senderr": false, + "headers": [], + "x": 2060, + "y": 320, + "wires": [ + [ + "65c3c2889a068c25", + "6248f46d1587526f" + ] + ] + }, + { + "id": "65c3c2889a068c25", + "type": "function", + "z": "8ccf34b55a2afcad", + "g": "1d1ce0cb54c52345", + "name": "Upsert work orders to local DB", + "func": "let payload = msg.payload;\n\nif (Buffer.isBuffer(payload)) {\n payload = payload.toString(\"utf8\");\n}\n\nif (typeof payload === \"string\") {\n try {\n payload = JSON.parse(payload);\n } catch (err) {\n payload = {};\n }\n}\n\nif (!payload || payload.ok === false) {\n node.status({\n fill: \"red\",\n shape: \"ring\",\n text: \"Work orders fetch failed\",\n });\n return null;\n}\n\nconst list = Array.isArray(payload.workOrders)\n ? payload.workOrders\n : Array.isArray(payload.orders)\n ? payload.orders\n : [];\n\nif (!list.length) {\n node.status({\n fill: \"yellow\",\n shape: \"ring\",\n text: \"No work orders\",\n });\n return null;\n}\n\nconst seen = new Set();\nconst values = [];\n\nlist.forEach((order) => {\n const id = String(\n order.workOrderId ||\n order.id ||\n order.work_order_id ||\n \"\"\n ).trim();\n\n if (!id || seen.has(id)) return;\n seen.add(id);\n\n const sku = String(order.sku || \"\").trim();\n\n const targetQtyRaw =\n order.targetQty ??\n order.target_qty ??\n order.target ??\n 0;\n\n const cycleTimeRaw =\n order.cycleTime ??\n order.theoreticalCycleTime ??\n order.theoretical_cycle_time ??\n 0;\n\n const targetQty = Number.isFinite(Number(targetQtyRaw))\n ? Math.trunc(Number(targetQtyRaw))\n : 0;\n\n const cycleTime = Number.isFinite(Number(cycleTimeRaw))\n ? Number(cycleTimeRaw)\n : 0;\n\n values.push([id, sku, targetQty, cycleTime, \"PENDING\"]);\n});\n\nif (!values.length) {\n node.status({\n fill: \"yellow\",\n shape: \"ring\",\n text: \"No valid work orders\",\n });\n return null;\n}\n\nmsg.topic = `\n INSERT INTO work_orders\n (work_order_id, sku, target_qty, cycle_time, status)\n VALUES ?\n ON DUPLICATE KEY UPDATE\n work_order_id = work_order_id;\n`;\n\nmsg.payload = [values];\nmsg._mode = \"sync-work-orders\";\n\nnode.status({\n fill: \"green\",\n shape: \"dot\",\n text: `Synced ${values.length} work orders`,\n});\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2310, + "y": 320, + "wires": [ + [ + "037e4825e2537ed2", + "13972bfb31607398" + ] + ] + }, + { + "id": "f983898f9906d911", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 15", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1670, + "y": 1340, + "wires": [] + }, + { + "id": "6248f46d1587526f", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 16", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1860, + "y": 1300, + "wires": [] + }, + { + "id": "13972bfb31607398", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 17", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2020, + "y": 1300, + "wires": [] + }, + { + "id": "1d01fcfb16429d32", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "Refresh work orders (poll)", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "86400", + "crontab": "", + "once": true, + "onceDelay": 1, + "topic": "workOrdersRefresh", + "payload": "", + "payloadType": "date", + "x": 1350, + "y": 1240, + "wires": [ + [ + "8abddc40064f1f64" + ] + ] + }, + { + "id": "17b04a44024da6f2", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 18", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1690, + "y": 1120, + "wires": [] + }, + { + "id": "254622f455de3a4c", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 19", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2270, + "y": 1220, + "wires": [] + }, + { + "id": "7bd006e12583f8e1", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 20", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 960, + "y": 220, + "wires": [] + }, + { + "id": "fa4a2d371b22788a", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 21", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1710, + "y": 1060, + "wires": [] + }, + { + "id": "4ea28603310c2aef", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 22", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1770, + "y": 1000, + "wires": [] + }, + { + "id": "30d6d75548f0bc28", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "function 3", + "func": "const awo = msg.payload?.activeWorkOrder;\nif (awo) {\n const v = awo.lastUpdateIso;\n\n // If it's already a string, keep it\n if (typeof v === \"string\") {\n // ok\n } else if (v instanceof Date) {\n awo.lastUpdateIso = v.toISOString();\n } else if (v && typeof v.toISO === \"function\") {\n // Luxon DateTime\n awo.lastUpdateIso = v.toISO();\n } else if (v && typeof v.toISOString === \"function\") {\n // Some date-like objects\n awo.lastUpdateIso = v.toISOString();\n } else if (v && typeof v.format === \"function\") {\n // Moment-like\n awo.lastUpdateIso = v.format();\n } else {\n // Last resort: try to stringify, otherwise null it out\n awo.lastUpdateIso = v ? String(v) : null;\n }\n node.warn(`[lastUpdateIso] typeof=${typeof v} value=${v}`);\n node.warn(`[lastUpdateIso] JSON=${JSON.stringify(v)}`);\n}\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3560, + "y": 740, + "wires": [ + [ + "3c747f2b199404f6" + ] + ] + }, + { + "id": "0d19192a0f6a11dd", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Build Heartbeat HTTP", + "func": "// Build direct heartbeat HTTP request\nconst config = global.get(\"config\") || {};\nconst base = config.cloudBaseUrl;\nconst apiKey = config.apiKey;\n\nif (!base || !apiKey) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Heartbeat waiting for pairing\" });\n return null;\n}\n\nconst baseUrl = String(base).replace(/\\/+$/, \"\");\nmsg.method = \"POST\";\nmsg.url = baseUrl + \"/api/ingest/heartbeat\";\nmsg.headers = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": apiKey,\n};\n\n// Use the same payload you already build in Online HeartBeat\nmsg.payload = {\n machineId: msg.machineId,\n tsDevice: msg.tsMs,\n tsMs: msg.tsMs, // device time; same as tsDevice for preprocess\n status: \"ONLINE\",\n message: \"NR heartbeat\",\n ip: (config.edgeIp || msg.ip || \"192.168.18.33\"),\n fwVersion: (config.fwVersion || \"raspi-nodered-1.0\"),\n};\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2800, + "y": 640, + "wires": [ + [ + "580d486714edfc8c" + ] + ] + }, + { + "id": "580d486714edfc8c", + "type": "http request", + "z": "8ccf34b55a2afcad", + "name": "Send Heartbeat", + "method": "use", + "ret": "txt", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "insecureHTTPParser": false, + "authType": "", + "senderr": false, + "headers": [], + "x": 3040, + "y": 640, + "wires": [ + [ + "17aa949bd6c8b667" + ] + ] + }, + { + "id": "17aa949bd6c8b667", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 24", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3210, + "y": 660, + "wires": [] + }, + { + "id": "c3ef5e9b98a8e24f", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 25", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 970, + "y": 380, + "wires": [] + }, + { + "id": "5c8843053781fbd9", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 26", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 960, + "y": 420, + "wires": [] + }, + { + "id": "fddb2e595128c225", + "type": "inject", + "z": "8ccf34b55a2afcad", + "g": "c95c05b78d8464c5", + "name": "", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": 0.1, + "topic": "", + "payload": "1", + "payloadType": "num", + "x": 1260, + "y": 240, + "wires": [ + [ + "6790210592277c61" + ] + ] + }, + { + "id": "6790210592277c61", + "type": "function", + "z": "8ccf34b55a2afcad", + "d": true, + "g": "c95c05b78d8464c5", + "name": "Simula Inyectora", + "func": "/**\n * Machine Cycle Simulator (0/1 square wave with realistic variance)\n *\n * How to use:\n * - Trigger this node ONCE (Inject once after deploy). It will start emitting 0/1 on its own.\n * - To stop: send a message with msg.payload = \"stop\" (or msg.topic=\"stop\")\n * - To reset state: msg.payload = \"reset\"\n *\n * Output:\n * - msg.payload = 0 or 1\n */\n\nconst CFG = {\n halfPeriodMs: 7250,\n jitterMs: 250,\n perfectRunMs: 30 * 60 * 1000,\n pEnterPerfect: 0, // was 0.12 — disable perfect runs\n pSlowPhase: 0, // was 0.10 — disable slow phases\n pMicroStop: 0.5, // was 0.06 — force frequent microstops\n pMacroStop: 0.2, // was 0.008 — force frequent macrostops\n microStopMs: [5000, 15000],\n macroStopMs: [60 * 1000, 120 * 1000], // shorter macros (1-2 min) so you don't wait 8 min\n slowFactor: [1.25, 1.9],\n slowPhaseToggles: [6, 25],\n minGapMicroMs: 10 * 1000, // was 20s — allow faster retriggering\n minGapMacroMs: 30 * 1000, // was 60s\n};\n\nfunction randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }\nfunction randFloat(min, max) { return Math.random() * (max - min) + min; }\nfunction pickMs(range) { return randInt(range[0], range[1]); }\nfunction clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }\n\nfunction clearTimer() {\n const t = context.get(\"timer\");\n if (t) clearTimeout(t);\n context.set(\"timer\", null);\n context.set(\"running\", false);\n}\n\nif (msg && (msg.payload === \"stop\" || msg.topic === \"stop\")) {\n clearTimer();\n node.status({ fill: \"grey\", shape: \"ring\", text: \"stopped\" });\n return null;\n}\n\nif (msg && msg.payload === \"reset\") {\n context.set(\"state\", 0);\n node.status({ fill: \"grey\", shape: \"dot\", text: \"reset to 0\" });\n return null;\n}\n\n// Store a template msg (so you can pass machineId/topic/etc. once)\nif (msg && typeof msg === \"object\") {\n const template = Object.assign({}, msg);\n delete template.payload; // we'll overwrite payload each emit\n context.set(\"template\", template);\n}\n\nlet running = context.get(\"running\") || false;\nlet timer = context.get(\"timer\");\nlet state = context.get(\"state\");\nif (state === undefined || state === null) state = 0;\n\nlet phase = context.get(\"phase\") || { type: \"idle\", until: 0, slowLeft: 0 };\nlet lastStopAt = context.get(\"lastStopAt\") || 0;\n\nfunction decideNextDelayMs() {\n const now = Date.now();\n\n // Perfect run active\n if (phase.type === \"perfect\" && now < phase.until) {\n const jitter = randInt(-CFG.jitterMs, CFG.jitterMs);\n return clamp(CFG.halfPeriodMs + jitter, 300, 30 * 60 * 1000);\n }\n\n // Slow phase active (counted in toggles)\n if (phase.type === \"slow\" && phase.slowLeft > 0) {\n phase.slowLeft -= 1;\n context.set(\"phase\", phase);\n\n const factor = randFloat(CFG.slowFactor[0], CFG.slowFactor[1]);\n const jitter = randInt(-CFG.jitterMs, CFG.jitterMs);\n node.status({ fill: \"blue\", shape: \"dot\", text: `slow (${phase.slowLeft} left)` });\n return clamp(Math.round(CFG.halfPeriodMs * factor) + jitter, 300, 30 * 60 * 1000);\n }\n\n // Phase ended → maybe enter a new perfect run\n if (Math.random() < CFG.pEnterPerfect) {\n phase = { type: \"perfect\", until: now + CFG.perfectRunMs, slowLeft: 0 };\n context.set(\"phase\", phase);\n node.status({ fill: \"green\", shape: \"dot\", text: \"perfect run\" });\n return decideNextDelayMs();\n }\n\n // Event selection (macro > micro > slow > normal)\n const sinceStop = now - lastStopAt;\n\n if (sinceStop > CFG.minGapMacroMs && Math.random() < CFG.pMacroStop) {\n lastStopAt = now;\n context.set(\"lastStopAt\", lastStopAt);\n node.status({ fill: \"red\", shape: \"dot\", text: \"MACRO STOP\" });\n return pickMs(CFG.macroStopMs);\n }\n\n if (sinceStop > CFG.minGapMicroMs && Math.random() < CFG.pMicroStop) {\n lastStopAt = now;\n context.set(\"lastStopAt\", lastStopAt);\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"micro stop\" });\n return pickMs(CFG.microStopMs);\n }\n\n if (Math.random() < CFG.pSlowPhase) {\n phase = { type: \"slow\", until: 0, slowLeft: randInt(CFG.slowPhaseToggles[0], CFG.slowPhaseToggles[1]) };\n context.set(\"phase\", phase);\n return decideNextDelayMs();\n }\n\n // Normal running\n node.status({ fill: \"green\", shape: \"dot\", text: \"running\" });\n const jitter = randInt(-CFG.jitterMs, CFG.jitterMs);\n return clamp(CFG.halfPeriodMs + jitter, 300, 30 * 60 * 1000);\n}\n\nfunction emitTick() {\n // Toggle 0/1\n state = state ? 0 : 1;\n context.set(\"state\", state);\n\n const template = context.get(\"template\") || {};\n const out = Object.assign({}, template, { payload: state });\n\n node.send(out);\n\n // Schedule next emission\n const delay = decideNextDelayMs();\n const t = setTimeout(emitTick, delay);\n context.set(\"timer\", t);\n}\n\nif (!running) {\n context.set(\"running\", true);\n node.status({ fill: \"green\", shape: \"dot\", text: \"started\" });\n}\n\n// Don’t start multiple timers if you accidentally keep the old repeating inject\n// before\n// after — always start fresh on trigger; ignore stale post-deploy refs\nclearTimeout(context.get(\"timer\"));\ncontext.set(\"timer\", setTimeout(emitTick, 100));\n\nreturn null;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1440, + "y": 240, + "wires": [ + [ + "ad2b65978bbb790a" + ] + ] + }, + { + "id": "34fe41a1d67fb9cd", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Persist pairing config", + "func": "const result = msg.payload || {};\nif (!result.ok) return null;\n\nconst config = global.get(\"config\") || {};\nif (!config.machineId || !config.apiKey) return null;\n\nconst settings = global.get(\"settings\") || {};\nconst shifts = settings.shifts || [{ start: \"08:00\", end: \"16:00\" }];\n\nmsg.topic = `\nINSERT INTO current_config (\n machine_id,\n api_key,\n org_id,\n cloud_base_url,\n shifts_json,\n shift_change_comp_min,\n lunch_break_min,\n threshold_multiplier,\n oee_alert_threshold,\n mold_total,\n mold_active,\n updated_at\n) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())\nON DUPLICATE KEY UPDATE\n api_key=VALUES(api_key),\n org_id=VALUES(org_id),\n cloud_base_url=VALUES(cloud_base_url),\n updated_at=NOW();\n`.trim();\n\nmsg.payload = [\n config.machineId,\n config.apiKey,\n config.orgId || null,\n config.cloudBaseUrl || null,\n JSON.stringify(shifts),\n Number(settings.shiftChangeCompensation ?? 10),\n Number(settings.lunchBreakMinutes ?? 30),\n Number(settings.thresholdMultiplier ?? 1.5),\n Number(settings.oeeAlertThreshold ?? 90),\n Number(settings.moldTotal ?? 0),\n Number(settings.moldActive ?? 0)\n];\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1000, + "y": 1060, + "wires": [ + [ + "79b190464e080076", + "fc335baf28ecf342" + ] + ] + }, + { + "id": "79b190464e080076", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "mydb": "fc9634aabefee16b", + "name": "Mold Presets DB", + "x": 1230, + "y": 1060, + "wires": [ + [ + "809c53d80b2a3f11" + ] + ] + }, + { + "id": "8a1e0fb8d6145ea2", + "type": "mysql", + "z": "8ccf34b55a2afcad", + "mydb": "fc9634aabefee16b", + "name": "Mold Presets DB", + "x": 3850, + "y": 220, + "wires": [ + [ + "4130b31ead5d77cb", + "6f8ad0a90b896c34" + ] + ] + }, + { + "id": "8bc52997ba90bd62", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "load config from DB", + "func": "msg.topic = `\nSELECT machine_id, api_key, org_id, cloud_base_url\nFROM current_config\nWHERE machine_id IS NOT NULL AND machine_id <> '' AND api_key IS NOT NULL AND api_key <> ''\nORDER BY updated_at DESC\nLIMIT 1;\n`.trim();\nmsg.payload = [];\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3650, + "y": 220, + "wires": [ + [ + "8a1e0fb8d6145ea2" + ] + ] + }, + { + "id": "4130b31ead5d77cb", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "apply config from DB", + "func": "const rows = msg.payload;\n\nif (!Array.isArray(rows) || rows.length === 0) {\n msg._configRetry = (msg._configRetry || 0) + 1;\n msg._configLoaded = false;\n return [null, msg]; // retry path\n}\n\nconst row = rows[0] || {};\nconst machineId = (row.machine_id || \"\").toString().trim();\nconst apiKey = (row.api_key || \"\").toString().trim();\n\nif (!machineId || !apiKey) {\n msg._configRetry = (msg._configRetry || 0) + 1;\n msg._configLoaded = false;\n return [null, msg]; // retry path\n}\n\nif (msg._configRetry > 12) return [null, null]; // optional stop after 12 tries\n\nconst config = global.get(\"config\") || {};\nconfig.machineId = machineId;\nconfig.apiKey = apiKey;\nif (row.org_id) config.orgId = row.org_id;\nif (row.cloud_base_url) config.cloudBaseUrl = row.cloud_base_url;\nif (config.settingsReadOnly === undefined) config.settingsReadOnly = true;\nif (!config.mqttTopicPrefix) config.mqttTopicPrefix = \"mis\";\n\nglobal.set(\"config\", config);\nmsg._configLoaded = true;\nreturn [msg, null];\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 4080, + "y": 220, + "wires": [ + [], + [ + "306a6b6cc0df4564" + ] + ] + }, + { + "id": "5452d6525c3ff5a6", + "type": "function", + "z": "8ccf34b55a2afcad", + "d": true, + "name": "throttle + dedupe", + "func": "// KPI Gate: 1/min throttle + dedupe + fresh timestamp\nconst now = Date.now();\nconst minIntervalMs = 60000;\n\nconst payload = msg.outbox?.payload;\nif (!payload) return null;\n\n// stable signature (no tsMs)\nconst sig = JSON.stringify({\n workOrderId: payload.activeWorkOrder?.id || null,\n cycle_count: payload.cycle_count || 0,\n good_parts: payload.good_parts || 0,\n scrap_parts: payload.scrap_parts || 0,\n cavities: payload.cavities || null,\n cycleTime: payload.cycleTime || 0,\n actualCycleTime: payload.actualCycleTime || 0,\n trackingEnabled: !!payload.trackingEnabled,\n productionStarted: !!payload.productionStarted,\n kpis: payload.kpis || {},\n});\n\nconst lastSentAt = flow.get(\"kpi_lastSentAt\") || 0;\nconst lastSig = flow.get(\"kpi_lastSig\") || \"\";\n\n// Drop if too soon and unchanged\nif ((now - lastSentAt) < minIntervalMs && sig === lastSig) {\n return null;\n}\n\n// Update gate state\nflow.set(\"kpi_lastSentAt\", now);\nflow.set(\"kpi_lastSig\", sig);\n\n// Ensure fresh timestamp to avoid timeline weirdness\npayload.tsMs = now;\nmsg.outbox.payload = payload;\nmsg.tsMs = now;\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2410, + "y": 1080, + "wires": [ + [] + ] + }, + { + "id": "87158a0fe2a0f7af", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "KPI minute tick", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "60", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 2520, + "y": 840, + "wires": [ + [ + "de4db4c8eec41d88" + ] + ] + }, + { + "id": "de4db4c8eec41d88", + "type": "function", + "z": "8ccf34b55a2afcad", + "name": "Build KPI Minute Snapshot", + "func": "// Build KPI Minute Snapshot (single source of KPI outbox)\nconst config = global.get(\"config\") || {};\nconst state = global.get(\"state\") || {};\nconst machineId = config.machineId;\nif (!machineId) return null;\n\nconst now = Date.now();\n// Optional: align to minute boundary for clean timeline\nconst tsMs = now - (now % 60000);\n\nconst rawActive = state.activeWorkOrder || null;\nconst active = rawActive ? { ...rawActive } : null;\n\n// Normalize activeWorkOrder.lastUpdateIso to string if present\nif (active && active.lastUpdateIso != null) {\n const v = active.lastUpdateIso;\n if (typeof v === \"string\") {\n // ok\n } else if (v instanceof Date) {\n active.lastUpdateIso = v.toISOString();\n } else if (typeof v === \"number\") {\n active.lastUpdateIso = new Date(v).toISOString();\n } else if (v && typeof v.toISOString === \"function\") {\n active.lastUpdateIso = v.toISOString();\n } else if (v && typeof v.toISO === \"function\") {\n active.lastUpdateIso = v.toISO();\n } else if (v && typeof v.format === \"function\") {\n active.lastUpdateIso = v.format();\n } else {\n delete active.lastUpdateIso; // avoid invalid payload\n }\n}\n\nconst kpis = state.currentKPIs || { oee: 0, availability: 0, performance: 0, quality: 0 };\n\nconst cavitiesRaw = Number(\n active?.cavities ??\n state.lastMoldActive ??\n 0\n);\nconst cavities = cavitiesRaw > 0 ? cavitiesRaw : null;\n\nmsg.machineId = machineId;\nmsg.tsMs = tsMs;\n\nmsg.outbox = {\n type: \"kpi\",\n payload: {\n tsMs,\n activeWorkOrder: active,\n cycle_count: Number(state.cycleCount ?? 0),\n good_parts: Number(active?.goodParts ?? 0),\n scrap_parts: Number(active?.scrapParts ?? 0),\n cavities,\n cycleTime: Number(active?.cycleTime ?? state.lastCycleTime ?? 0),\n actualCycleTime: Number(state.lastActualCycleTime ?? 0),\n trackingEnabled: !!state.trackingEnabled,\n productionStarted: !!state.productionStarted,\n kpis,\n },\n};\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2740, + "y": 840, + "wires": [ + [ + "4e12fb078508f92e" + ] + ] + }, + { + "id": "a9fa224f48b83f71", + "type": "delay", + "z": "8ccf34b55a2afcad", + "name": "", + "pauseType": "delay", + "timeout": "2", + "timeoutUnits": "seconds", + "rate": "1", + "nbRateUnits": "1", + "rateUnits": "second", + "randomFirst": "1", + "randomLast": "5", + "randomUnits": "seconds", + "drop": false, + "allowrate": false, + "outputs": 1, + "x": 3460, + "y": 220, + "wires": [ + [ + "8bc52997ba90bd62" + ] + ] + }, + { + "id": "306a6b6cc0df4564", + "type": "switch", + "z": "8ccf34b55a2afcad", + "name": "", + "property": "config.apiKey", + "propertyType": "global", + "rules": [ + { + "t": "nempty" + }, + { + "t": "empty" + } + ], + "checkall": "true", + "repair": false, + "outputs": 2, + "x": 4290, + "y": 220, + "wires": [ + [], + [ + "a9fa224f48b83f71" + ] + ] + }, + { + "id": "fc335baf28ecf342", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 12", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1210, + "y": 1160, + "wires": [] + }, + { + "id": "809c53d80b2a3f11", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 27", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1050, + "y": 1240, + "wires": [] + }, + { + "id": "6f8ad0a90b896c34", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 13", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 4030, + "y": 140, + "wires": [] + }, + { + "id": "ad2b65978bbb790a", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 14", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2710, + "y": 460, + "wires": [] + }, + { + "id": "aa698e63d1f67845", + "type": "debug", + "z": "8ccf34b55a2afcad", + "name": "debug 23", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 2310, + "y": 60, + "wires": [] + }, + { + "id": "467cfdec8cf7257d", + "type": "16inpind", + "z": "8ccf34b55a2afcad", + "name": "", + "stack": "0", + "channel": "5", + "x": 2740, + "y": 120, + "wires": [ + [ + "9a2690258e151b66" + ] + ] + }, + { + "id": "21b91336f4d77c41", + "type": "inject", + "z": "8ccf34b55a2afcad", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "0.01", + "crontab": "", + "once": true, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 2550, + "y": 120, + "wires": [ + [ + "467cfdec8cf7257d" + ] + ] + }, + { + "id": "919b5b8d778e2b6c", + "type": "ui_group", + "name": "Default", + "tab": "c567195d86466cd5", + "order": 1, + "disp": false, + "width": "25", + "collapse": false, + "className": "" + }, + { + "id": "e2f3a4b5c6d7e8f9", + "type": "ui_group", + "name": "Alerts Group", + "tab": "a1b2c3d4e5f60718", + "order": 1, + "disp": false, + "width": "25", + "collapse": false, + "className": "" + }, + { + "id": "e3f4a5b6c7d8e9f0", + "type": "ui_group", + "name": "Graphs Group", + "tab": "b2c3d4e5f6a70182", + "order": 1, + "disp": false, + "width": "25", + "collapse": false, + "className": "" + }, + { + "id": "e4f5a6b7c8d9e0f1", + "type": "ui_group", + "name": "Help Group", + "tab": "c3d4e5f6a7b80192", + "order": 1, + "disp": false, + "width": "25", + "collapse": false, + "className": "" + }, + { + "id": "e5f6a7b8c9d0e1f2", + "type": "ui_group", + "name": "Settings Group", + "tab": "d4e5f6a7b8c90123", + "order": 1, + "disp": false, + "width": "25", + "collapse": false, + "className": "" + }, + { + "id": "b99c269687d574aa", + "type": "ui_group", + "name": "Work Orders Group", + "tab": "d1a1e2f3a4b5c6d7", + "order": 1, + "disp": false, + "width": 25, + "collapse": false, + "className": "" + }, + { + "id": "fc9634aabefee16b", + "type": "MySQLdatabase", + "name": "Edge Outbox", + "host": "127.0.0.1", + "port": "3306", + "db": "edge_outbox", + "tz": "", + "charset": "UTF8" + }, + { + "id": "c567195d86466cd5", + "type": "ui_tab", + "name": "Home", + "icon": "dashboard", + "order": 1, + "disabled": false, + "hidden": false + }, + { + "id": "a1b2c3d4e5f60718", + "type": "ui_tab", + "name": "Alerts", + "icon": "warning", + "order": 3, + "disabled": false, + "hidden": false + }, + { + "id": "b2c3d4e5f6a70182", + "type": "ui_tab", + "name": "Graphs", + "icon": "show_chart", + "order": 4, + "disabled": false, + "hidden": false + }, + { + "id": "c3d4e5f6a7b80192", + "type": "ui_tab", + "name": "Help", + "icon": "help", + "order": 5, + "disabled": false, + "hidden": false + }, + { + "id": "d4e5f6a7b8c90123", + "type": "ui_tab", + "name": "Settings", + "icon": "settings", + "order": 6, + "disabled": false, + "hidden": false + }, + { + "id": "d1a1e2f3a4b5c6d7", + "type": "ui_tab", + "name": "Work Orders", + "icon": "list", + "order": 2, + "disabled": false, + "hidden": false + }, + { + "id": "43d51056507542b5", + "type": "global-config", + "env": [], + "modules": { + "node-red-node-mysql": "2.0.0", + "node-red-dashboard": "3.6.6", + "node-red-contrib-spreadsheet-in": "0.7.2", + "node-red-node-pi-gpio": "2.0.6", + "node-red-contrib-sm-16inpind": "1.0.1" + } + } +] diff --git a/lib/recap/getRecapData.ts b/lib/recap/getRecapData.ts index 1261681..c148154 100644 --- a/lib/recap/getRecapData.ts +++ b/lib/recap/getRecapData.ts @@ -218,6 +218,8 @@ function eventIncidentKey(data: unknown, eventType: string, ts: Date) { const inner = extractEventData(data); const direct = String(inner.incidentKey ?? inner.incident_key ?? "").trim(); if (direct) return direct; + const alertId = String(inner.alert_id ?? inner.alertId ?? "").trim(); + if (alertId) return `${eventType}:${alertId}`; const startMs = safeNum(inner.start_ms) ?? safeNum(inner.startMs); if (startMs != null) return `${eventType}:${Math.trunc(startMs)}`; return `${eventType}:${ts.getTime()}`; @@ -291,7 +293,7 @@ async function computeRecap(params: Required> & { const machines = await prisma.machine.findMany({ where: { orgId: params.orgId, ...machineFilter }, orderBy: { name: "asc" }, - select: { id: true, name: true, location: true }, + select: { id: true, name: true, location: true, tsServer: true }, }); if (!machines.length) { @@ -421,9 +423,9 @@ async function computeRecap(params: Required> & { where: { orgId: params.orgId, machineId: { in: machineIds }, - ts: { lte: params.end }, + tsServer: { lte: params.end }, }, - orderBy: [{ machineId: "asc" }, { ts: "desc" }], + orderBy: [{ machineId: "asc" }, { tsServer: "desc" }], distinct: ["machineId"], select: { machineId: true, @@ -814,8 +816,36 @@ async function computeRecap(params: Required> & { ); } + const firstProductionMsAfterMoldStart = (startMs: number) => { + let best: number | null = null; + for (const cycle of dedupedCycles) { + const t = cycle.ts.getTime(); + if (t <= startMs) continue; + const g = safeNum(cycle.goodDelta) ?? 0; + const s = safeNum(cycle.scrapDelta) ?? 0; + if (g > 0 || s > 0) { + if (best == null || t < best) best = t; + } + } + for (const kpi of dedupedKpis) { + const t = kpi.ts.getTime(); + if (t <= startMs) continue; + const g = safeNum(kpi.good) ?? safeNum(kpi.goodParts) ?? 0; + const s = safeNum(kpi.scrap) ?? safeNum(kpi.scrapParts) ?? 0; + if (g > 0 || s > 0) { + if (best == null || t < best) best = t; + } + } + return best; + }; + const moldActiveByIncident = new Map(); for (const event of machineMoldEvents) { + const inner = extractEventData(event.data); + const isUpdate = safeBool(inner.is_update ?? inner.isUpdate); + const isAutoAck = safeBool(inner.is_auto_ack ?? inner.isAutoAck); + if (isUpdate || isAutoAck) continue; + const key = eventIncidentKey(event.data, "mold-change", event.ts); const status = eventStatus(event.data); if (status === "resolved") { @@ -827,6 +857,12 @@ async function computeRecap(params: Required> & { moldActiveByIncident.set(key, moldStartMs(event.data, event.ts)); } } + for (const [k, startMs] of [...moldActiveByIncident.entries()]) { + const resumeMs = firstProductionMsAfterMoldStart(startMs); + if (resumeMs != null && resumeMs <= params.end.getTime()) { + moldActiveByIncident.delete(k); + } + } let moldChangeStartMs: number | null = null; for (const startMs of moldActiveByIncident.values()) { if (moldChangeStartMs == null || startMs > moldChangeStartMs) moldChangeStartMs = startMs; @@ -879,7 +915,14 @@ async function computeRecap(params: Required> & { moldChangeStartMs, }, heartbeat: { - lastSeenAt: toIso(latestTs), + lastSeenAt: toIso( + (() => { + const hbMs = latestHb ? (latestHb.tsServer ?? latestHb.ts).getTime() : null; + const machineMs = machine.tsServer.getTime(); + if (hbMs != null) return new Date(Math.max(hbMs, machineMs)); + return machine.tsServer; + })() + ), uptimePct, }, }; diff --git a/lib/recap/progressDisplay.ts b/lib/recap/progressDisplay.ts new file mode 100644 index 0000000..bd9118a --- /dev/null +++ b/lib/recap/progressDisplay.ts @@ -0,0 +1,27 @@ +/** + * Recap & work-order progress: large targets (e.g. 301k) make raw % < 1. + * Rounding to integer shows 0%; bar width 0.17% is invisible. Use decimals + a visual floor for the bar. + */ + +/** "0.17%" with enough precision when needed; "—" for null. */ +export function formatRecapProgressPercent( + pct: number | null | undefined, + locale: string +): string { + if (pct == null || Number.isNaN(pct)) return "—"; + if (pct <= 0) return "0%"; + if (pct < 10) { + return `${pct.toLocaleString(locale, { maximumFractionDigits: 2, minimumFractionDigits: 0 })}%`; + } + return `${Math.round(pct).toLocaleString(locale)}%`; +} + +/** + * For CSS width %: keep proportional when ≥2%; below that, any positive progress + * needs a minimum or the bar looks like a single pixel. + */ +export function progressBarWidthPercent(pct: number | null | undefined): number { + if (pct == null || Number.isNaN(pct) || pct <= 0) return 0; + if (pct < 2) return Math.max(2, Math.min(100, pct)); + return Math.min(100, pct); +} diff --git a/lib/recap/recapUiConstants.ts b/lib/recap/recapUiConstants.ts new file mode 100644 index 0000000..3e390fe --- /dev/null +++ b/lib/recap/recapUiConstants.ts @@ -0,0 +1,4 @@ +/** + * Client-safe recap thresholds. Kept in sync with OFFLINE logic in lib/recap/redesign.ts. + */ +export const RECAP_HEARTBEAT_STALE_MS = 10 * 60 * 1000; diff --git a/lib/recap/redesign.ts b/lib/recap/redesign.ts index 5291b27..b317fe4 100644 --- a/lib/recap/redesign.ts +++ b/lib/recap/redesign.ts @@ -9,6 +9,7 @@ import { type TimelineCycleRow, type TimelineEventRow, } from "@/lib/recap/timeline"; +import { RECAP_HEARTBEAT_STALE_MS } from "@/lib/recap/recapUiConstants"; import type { RecapDetailResponse, RecapMachine, @@ -25,7 +26,7 @@ type DetailRangeInput = { end?: string | null; }; -const OFFLINE_THRESHOLD_MS = 10 * 60 * 1000; +const OFFLINE_THRESHOLD_MS = RECAP_HEARTBEAT_STALE_MS; const TIMELINE_EVENT_LOOKBACK_MS = 24 * 60 * 60 * 1000; const RECAP_CACHE_TTL_SEC = 60; const WEEKDAY_KEYS: ShiftOverrideDay[] = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]; diff --git a/mis-control-tower@0.1.0 b/mis-control-tower@0.1.0 new file mode 100644 index 0000000..e69de29 diff --git a/next b/next new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 90cbc87..5e8c4e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/nodemailer": "^7.0.4", "@types/react": "^19", "@types/react-dom": "^19", + "baseline-browser-mapping": "^2.10.22", "dotenv-cli": "^11.0.0", "eslint": "^9", "eslint-config-next": "16.0.10", @@ -38,8 +39,6 @@ }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "dev": true, "license": "MIT", "engines": { @@ -51,8 +50,6 @@ }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -67,8 +64,6 @@ }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -80,8 +75,6 @@ }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -94,8 +87,6 @@ }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -108,8 +99,6 @@ }, "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -123,8 +112,6 @@ }, "node_modules/@aws-crypto/supports-web-crypto": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -133,8 +120,6 @@ }, "node_modules/@aws-crypto/util": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -145,8 +130,6 @@ }, "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -158,8 +141,6 @@ }, "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -172,8 +153,6 @@ }, "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -186,8 +165,6 @@ }, "node_modules/@aws-sdk/client-sesv2": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.968.0.tgz", - "integrity": "sha512-vuzF/4Ovzv2UW2iVVMNSu3yIIczzdUKBkkiXTvYYRmOL4Kjtq7RLu8A8O6jy+/mJoWW1CTyZH9pTc4MCQzjLIA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -238,8 +215,6 @@ }, "node_modules/@aws-sdk/client-sso": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.968.0.tgz", - "integrity": "sha512-y+k23MvMzpn1WpeQ9sdEXg1Bbw7dfi0ZH2uwyBv78F/kz0mZOI+RJ1KJg8DgSD8XvdxB8gX5GQ8rzo0LnDothA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -288,8 +263,6 @@ }, "node_modules/@aws-sdk/core": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.968.0.tgz", - "integrity": "sha512-u4lIpvGqMMHZN523/RxW70xNoVXHBXucIWZsxFKc373E6TWYEb16ddFhXTELioS5TU93qkd/6yDQZzI6AAhbkw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -313,8 +286,6 @@ }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.968.0.tgz", - "integrity": "sha512-G+zgXEniQxBHFtHo+0yImkYutvJZLvWqvkPUP8/cG+IaYg54OY7L/GPIAZJh0U3m0Uepao98NbL15zjM+uplqQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -330,8 +301,6 @@ }, "node_modules/@aws-sdk/credential-provider-http": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.968.0.tgz", - "integrity": "sha512-79teHBx/EtsNRR3Bq8fQdmMHtUcYwvohm9EwXXFt2Jd3BEOBH872IjIlfKdAvdkM+jW1QeeWOZBAxXGPir7GcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -352,8 +321,6 @@ }, "node_modules/@aws-sdk/credential-provider-ini": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.968.0.tgz", - "integrity": "sha512-9J9pcweoEN8yG7Qliux1zl9J3DT8X6OLcDN2RVXdTd5xzWBaYlupnUiJzoP6lvXdMnEmlDZaV7IMtoBdG7MY6g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -378,8 +345,6 @@ }, "node_modules/@aws-sdk/credential-provider-login": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.968.0.tgz", - "integrity": "sha512-YxBaR0IMuHPOVTG+73Ve0QfllweN+EdwBRnHFhUGnahMGAcTmcaRdotqwqWfiws+9ud44IFKjxXR3t8jaGpFnQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -398,8 +363,6 @@ }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.968.0.tgz", - "integrity": "sha512-wei6v0c9vDEam8pM5eWe9bt+5ixg8nL0q+DFPzI6iwdLUqmJsPoAzWPEyMkgp03iE02SS2fMqPWpmRjz/NVyUw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -422,8 +385,6 @@ }, "node_modules/@aws-sdk/credential-provider-process": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.968.0.tgz", - "integrity": "sha512-my9M/ijRyEACoyeEWiC2sTVM3+eck5IWPGTPQrlYMKivy4LLlZchohtIopuqTom+JZzLZD508j1s9aDvl7BA0w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -440,8 +401,6 @@ }, "node_modules/@aws-sdk/credential-provider-sso": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.968.0.tgz", - "integrity": "sha512-XPYPcxfWIt5jBbofoP2xhAHlFYos0dzwbHsoE18Cera/XnaCEbsUpdROo30t0Kjdbv0EWMYLMPDi9G+vPRDnhQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -460,8 +419,6 @@ }, "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.968.0.tgz", - "integrity": "sha512-9HNAP6mx2jsBW4moWnRg5ycyZ0C1EbtMIegIHa93ga13B/8VZF9Y0iDnwW73yQYzCEt9UrDiFeRck/ChZup3rA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -479,8 +436,6 @@ }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.968.0.tgz", - "integrity": "sha512-ujlNT215VtE/2D2jEhFVcTuPPB36HJyLBM0ytnni/WPIjzq89iJrKR1tEhxpk8uct6A5NSQ6w9Y7g2Rw1rkSoQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -495,8 +450,6 @@ }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.968.0.tgz", - "integrity": "sha512-zvhhEPZgvaRDxzf27m2WmgaXoN7upFt/gvG7ofBN5zCBlkh3JtFamMh5KWYVQwMhc4eQBK3NjH0oIUKZSVztag==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -510,8 +463,6 @@ }, "node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.968.0.tgz", - "integrity": "sha512-KygPiwpSAPGobgodK/oLb7OLiwK29pNJeNtP+GZ9pxpceDRqhN0Ub8Eo84dBbWq+jbzAqBYHzy+B1VsbQ/hLWA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -527,8 +478,6 @@ }, "node_modules/@aws-sdk/middleware-sdk-s3": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.968.0.tgz", - "integrity": "sha512-fh2mQ/uwJ1Sth1q2dWAbeyky/SBPaqe1fjxvsNeEY6dtfi8PjW85zHpz1JoAhCKTRkrEdXYAqkqUwsUydLucyQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -553,8 +502,6 @@ }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.968.0.tgz", - "integrity": "sha512-4h5/B8FyxMjLxtXd5jbM2R69aO57qQiHoAJQTtkpuxmM7vhvjSxEQtMM9L1kuMXoMVNE7xM4886h0+gbmmxplg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -572,8 +519,6 @@ }, "node_modules/@aws-sdk/nested-clients": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.968.0.tgz", - "integrity": "sha512-LLppm+8MzD3afD2IA/tYDp5AoVPOybK7MHQz5DVB4HsZ+fHvwYlvau2ZUK8nKwJSk5c1kWcxCZkyuJQjFu37ng==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -622,8 +567,6 @@ }, "node_modules/@aws-sdk/region-config-resolver": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.968.0.tgz", - "integrity": "sha512-BzrCpxEsAHbi+yDGtgXJ+/5AvLPjfhcT6DlL+Fc4g13J5Z0VwiO95Wem+Q4KK7WDZH7/sZ/1WFvfitjLTKZbEw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -639,8 +582,6 @@ }, "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.968.0.tgz", - "integrity": "sha512-kRBA1KK3LTHnfYJLPsESNF2WhQN6DyGc9MiM6qG8AdJwMPQkanF5hwtckV1ToO2KB5v1q+1PuvBvy6Npd2IV+w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -657,8 +598,6 @@ }, "node_modules/@aws-sdk/token-providers": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.968.0.tgz", - "integrity": "sha512-lXUZqB2qTFmZYNXPnVT0suSHGiuQAPrL2DhmhbjqOdR7+GKDHL5KbeKFvPisy7Y4neliJqT4Q1VPWa0nqYaiZg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -676,8 +615,6 @@ }, "node_modules/@aws-sdk/types": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.968.0.tgz", - "integrity": "sha512-Wuumj/1cuiuXTMdHmvH88zbEl+5Pw++fOFQuMCF4yP0R+9k1lwX8rVst+oy99xaxtdluJZXrsccoZoA67ST1Ow==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -690,8 +627,6 @@ }, "node_modules/@aws-sdk/util-arn-parser": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.968.0.tgz", - "integrity": "sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -703,8 +638,6 @@ }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.968.0.tgz", - "integrity": "sha512-9IdilgylS0crFSeI59vtr8qhDYMYYOvnvkl1dLp59+EmLH1IdXz7+4cR5oh5PkoqD7DRzc5Uzm2GnZhK6I0oVQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -720,8 +653,6 @@ }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.965.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.2.tgz", - "integrity": "sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -733,8 +664,6 @@ }, "node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.968.0.tgz", - "integrity": "sha512-nRxjs8Jpq8ZHFsa/0uiww2f4+40D6Dt6bQmepAJHIE/D+atwPINDKsfamCjFnxrjKU3WBWpGYEf/QDO0XZsFMw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -746,8 +675,6 @@ }, "node_modules/@aws-sdk/util-user-agent-node": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.968.0.tgz", - "integrity": "sha512-oaIkPGraGhZgkDmxVhTIlakaUNWKO9aMN+uB6I+eS26MWi/lpMK66HTZeXEnaTrmt5/kl99YC0N37zScz58Tdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -771,8 +698,6 @@ }, "node_modules/@aws-sdk/xml-builder": { "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.968.0.tgz", - "integrity": "sha512-bZQKn41ebPh/uW9uWUE5oLuaBr44Gt78dkw2amu5zcwo1J/d8s6FdzZcRDmz0rHE2NHJWYkdQYeVQo7jhMziqA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -786,8 +711,6 @@ }, "node_modules/@aws/lambda-invoke-store": { "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", - "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -796,8 +719,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -811,8 +732,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "dev": true, "license": "MIT", "engines": { @@ -821,8 +740,6 @@ }, "node_modules/@babel/core": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", "dependencies": { @@ -852,8 +769,6 @@ }, "node_modules/@babel/generator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, "license": "MIT", "dependencies": { @@ -869,8 +784,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { @@ -886,8 +799,6 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { @@ -896,8 +807,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { @@ -910,8 +819,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { @@ -928,8 +835,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -938,8 +843,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -948,8 +851,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -958,8 +859,6 @@ }, "node_modules/@babel/helpers": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { @@ -972,8 +871,6 @@ }, "node_modules/@babel/parser": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -988,8 +885,6 @@ }, "node_modules/@babel/runtime": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -997,8 +892,6 @@ }, "node_modules/@babel/template": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1012,8 +905,6 @@ }, "node_modules/@babel/traverse": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "dev": true, "license": "MIT", "dependencies": { @@ -1031,8 +922,6 @@ }, "node_modules/@babel/types": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1043,43 +932,8 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1097,8 +951,6 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1110,8 +962,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -1120,8 +970,6 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1135,8 +983,6 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1148,8 +994,6 @@ }, "node_modules/@eslint/core": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1161,8 +1005,6 @@ }, "node_modules/@eslint/eslintrc": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1185,8 +1027,6 @@ }, "node_modules/@eslint/js": { "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -1198,8 +1038,6 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1208,8 +1046,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1222,8 +1058,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1232,8 +1066,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1246,8 +1078,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1260,8 +1090,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1274,174 +1102,14 @@ }, "node_modules/@img/colour": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "license": "MIT", "optional": true, "engines": { "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linux-x64": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -1454,26 +1122,8 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -1486,120 +1136,8 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, "node_modules/@img/sharp-linux-x64": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -1618,32 +1156,8 @@ "@img/sharp-libvips-linux-x64": "1.2.4" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, "node_modules/@img/sharp-linuxmusl-x64": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -1662,86 +1176,8 @@ "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -1751,8 +1187,6 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1762,8 +1196,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -1772,15 +1204,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1790,8 +1218,6 @@ }, "node_modules/@messageformat/core": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz", - "integrity": "sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==", "license": "MIT", "dependencies": { "@messageformat/date-skeleton": "^1.0.0", @@ -1804,20 +1230,14 @@ }, "node_modules/@messageformat/date-skeleton": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz", - "integrity": "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==", "license": "MIT" }, "node_modules/@messageformat/number-skeleton": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz", - "integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==", "license": "MIT" }, "node_modules/@messageformat/parser": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.1.tgz", - "integrity": "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==", "license": "MIT", "dependencies": { "moo": "^0.5.1" @@ -1825,110 +1245,25 @@ }, "node_modules/@messageformat/runtime": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.2.tgz", - "integrity": "sha512-dkIPDCjXcfhSHgNE1/qV6TeczQZR59Yx0xXeafVKgK3QVWoxc38ljwpksUpnzCGvN151KUbCJTDZVmahtf1YZw==", "license": "MIT", "dependencies": { "make-plural": "^7.0.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, "node_modules/@next/env": { "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", - "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.10.tgz", - "integrity": "sha512-b2NlWN70bbPLmfyoLvvidPKWENBYYIe017ZGUpElvQjDytCWgxPJx7L9juxHt0xHvNVA08ZHJdOyhGzon/KJuw==", "dev": true, "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz", - "integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz", - "integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz", - "integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz", - "integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-linux-x64-gnu": { "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz", - "integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==", "cpu": [ "x64" ], @@ -1943,8 +1278,6 @@ }, "node_modules/@next/swc-linux-x64-musl": { "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz", - "integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==", "cpu": [ "x64" ], @@ -1957,42 +1290,8 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz", - "integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.10", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz", - "integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -2005,8 +1304,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -2015,8 +1312,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -2029,8 +1324,6 @@ }, "node_modules/@nolyfill/is-core-module": { "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", "dev": true, "license": "MIT", "engines": { @@ -2039,8 +1332,6 @@ }, "node_modules/@prisma/client": { "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", - "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -2061,8 +1352,6 @@ }, "node_modules/@prisma/config": { "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", - "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -2074,15 +1363,11 @@ }, "node_modules/@prisma/debug": { "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", - "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", - "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -2095,15 +1380,11 @@ }, "node_modules/@prisma/engines-version": { "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", - "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", - "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -2114,8 +1395,6 @@ }, "node_modules/@prisma/get-platform": { "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", - "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -2124,8 +1403,6 @@ }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -2150,8 +1427,6 @@ }, "node_modules/@reduxjs/toolkit/node_modules/immer": { "version": "11.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", - "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", "license": "MIT", "funding": { "type": "opencollective", @@ -2160,15 +1435,11 @@ }, "node_modules/@rtsao/scc": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, "license": "MIT" }, "node_modules/@smithy/abort-controller": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", - "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2181,8 +1452,6 @@ }, "node_modules/@smithy/config-resolver": { "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", - "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2199,8 +1468,6 @@ }, "node_modules/@smithy/core": { "version": "3.20.5", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.5.tgz", - "integrity": "sha512-0Tz77Td8ynHaowXfOdrD0F1IH4tgWGUhwmLwmpFyTbr+U9WHXNNp9u/k2VjBXGnSe7BwjBERRpXsokGTXzNjhA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2221,8 +1488,6 @@ }, "node_modules/@smithy/credential-provider-imds": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", - "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2238,8 +1503,6 @@ }, "node_modules/@smithy/fetch-http-handler": { "version": "5.3.9", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", - "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2255,8 +1518,6 @@ }, "node_modules/@smithy/hash-node": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", - "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2271,8 +1532,6 @@ }, "node_modules/@smithy/invalid-dependency": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", - "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2285,8 +1544,6 @@ }, "node_modules/@smithy/is-array-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2298,8 +1555,6 @@ }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", - "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2313,8 +1568,6 @@ }, "node_modules/@smithy/middleware-endpoint": { "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.6.tgz", - "integrity": "sha512-dpq3bHqbEOBqGBjRVHVFP3eUSPpX0BYtg1D5d5Irgk6orGGAuZfY22rC4sErhg+ZfY/Y0kPqm1XpAmDZg7DeuA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2333,8 +1586,6 @@ }, "node_modules/@smithy/middleware-retry": { "version": "4.4.22", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.22.tgz", - "integrity": "sha512-vwWDMaObSMjw6WCC/3Ae9G7uul5Sk95jr07CDk1gkIMpaDic0phPS1MpVAZ6+YkF7PAzRlpsDjxPwRlh/S11FQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2354,8 +1605,6 @@ }, "node_modules/@smithy/middleware-serde": { "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", - "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2369,8 +1618,6 @@ }, "node_modules/@smithy/middleware-stack": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", - "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2383,8 +1630,6 @@ }, "node_modules/@smithy/node-config-provider": { "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", - "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2399,8 +1644,6 @@ }, "node_modules/@smithy/node-http-handler": { "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz", - "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2416,8 +1659,6 @@ }, "node_modules/@smithy/property-provider": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", - "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2430,8 +1671,6 @@ }, "node_modules/@smithy/protocol-http": { "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", - "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2444,8 +1683,6 @@ }, "node_modules/@smithy/querystring-builder": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", - "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2459,8 +1696,6 @@ }, "node_modules/@smithy/querystring-parser": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", - "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2473,8 +1708,6 @@ }, "node_modules/@smithy/service-error-classification": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", - "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2486,8 +1719,6 @@ }, "node_modules/@smithy/shared-ini-file-loader": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", - "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2500,8 +1731,6 @@ }, "node_modules/@smithy/signature-v4": { "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", - "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2520,8 +1749,6 @@ }, "node_modules/@smithy/smithy-client": { "version": "4.10.7", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.7.tgz", - "integrity": "sha512-Uznt0I9z3os3Z+8pbXrOSCTXCA6vrjyN7Ub+8l2pRDum44vLv8qw0qGVkJN0/tZBZotaEFHrDPKUoPNueTr5Vg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2539,8 +1766,6 @@ }, "node_modules/@smithy/types": { "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", - "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2552,8 +1777,6 @@ }, "node_modules/@smithy/url-parser": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", - "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2567,8 +1790,6 @@ }, "node_modules/@smithy/util-base64": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2582,8 +1803,6 @@ }, "node_modules/@smithy/util-body-length-browser": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2595,8 +1814,6 @@ }, "node_modules/@smithy/util-body-length-node": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2608,8 +1825,6 @@ }, "node_modules/@smithy/util-buffer-from": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2622,8 +1837,6 @@ }, "node_modules/@smithy/util-config-provider": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2635,8 +1848,6 @@ }, "node_modules/@smithy/util-defaults-mode-browser": { "version": "4.3.21", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.21.tgz", - "integrity": "sha512-DtmVJarzqtjghtGjCw/PFJolcJkP7GkZgy+hWTAN3YLXNH+IC82uMoMhFoC3ZtIz5mOgCm5+hOGi1wfhVYgrxw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2651,8 +1862,6 @@ }, "node_modules/@smithy/util-defaults-mode-node": { "version": "4.2.24", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.24.tgz", - "integrity": "sha512-JelBDKPAVswVY666rezBvY6b0nF/v9TXjUbNwDNAyme7qqKYEX687wJv0uze8lBIZVbg30wlWnlYfVSjjpKYFA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2670,8 +1879,6 @@ }, "node_modules/@smithy/util-endpoints": { "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", - "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2685,8 +1892,6 @@ }, "node_modules/@smithy/util-hex-encoding": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2698,8 +1903,6 @@ }, "node_modules/@smithy/util-middleware": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", - "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2712,8 +1915,6 @@ }, "node_modules/@smithy/util-retry": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", - "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2727,8 +1928,6 @@ }, "node_modules/@smithy/util-stream": { "version": "4.5.10", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz", - "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2747,8 +1946,6 @@ }, "node_modules/@smithy/util-uri-escape": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2760,8 +1957,6 @@ }, "node_modules/@smithy/util-utf8": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2774,8 +1969,6 @@ }, "node_modules/@smithy/uuid": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2787,20 +1980,14 @@ }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, "node_modules/@standard-schema/utils": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, "node_modules/@swc/helpers": { "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -2808,8 +1995,6 @@ }, "node_modules/@tailwindcss/node": { "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2824,8 +2009,6 @@ }, "node_modules/@tailwindcss/oxide": { "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", "dev": true, "license": "MIT", "engines": { @@ -2846,129 +2029,8 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", "cpu": [ "x64" ], @@ -2984,8 +2046,6 @@ }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", "cpu": [ "x64" ], @@ -2999,74 +2059,8 @@ "node": ">= 10" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@tailwindcss/postcss": { "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", "dev": true, "license": "MIT", "dependencies": { @@ -3077,21 +2071,8 @@ "tailwindcss": "4.1.18" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/bcrypt": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3100,26 +2081,18 @@ }, "node_modules/@types/d3-array": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -3127,14 +2100,10 @@ }, "node_modules/@types/d3-path": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -3142,8 +2111,6 @@ }, "node_modules/@types/d3-shape": { "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -3151,41 +2118,29 @@ }, "node_modules/@types/d3-time": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "20.19.29", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.29.tgz", - "integrity": "sha512-YrT9ArrGaHForBaCNwFjoqJWmn8G1Pr7+BH/vwyLHciA9qT/wSiuOhxGCT50JA5xLvFBd6PIiGkE3afxcPE1nw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3193,8 +2148,6 @@ }, "node_modules/@types/nodemailer": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.5.tgz", - "integrity": "sha512-7WtR4MFJUNN2UFy0NIowBRJswj5KXjXDhlZY43Hmots5eGu5q/dTeFd/I6GgJA/qj3RqO6dDy4SvfcV3fOVeIA==", "dev": true, "license": "MIT", "dependencies": { @@ -3204,8 +2157,6 @@ }, "node_modules/@types/react": { "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", - "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3214,8 +2165,6 @@ }, "node_modules/@types/react-dom": { "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3224,8 +2173,6 @@ }, "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": "*" @@ -3233,14 +2180,10 @@ }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", - "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==", "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": "*" @@ -3248,8 +2191,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", - "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==", "dev": true, "license": "MIT", "dependencies": { @@ -3277,8 +2218,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -3287,8 +2226,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz", - "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", "dependencies": { @@ -3312,8 +2249,6 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", - "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", "dev": true, "license": "MIT", "dependencies": { @@ -3334,8 +2269,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", - "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", "dev": true, "license": "MIT", "dependencies": { @@ -3352,8 +2285,6 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", - "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", "dev": true, "license": "MIT", "engines": { @@ -3369,8 +2300,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz", - "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==", "dev": true, "license": "MIT", "dependencies": { @@ -3394,8 +2323,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", - "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", "dev": true, "license": "MIT", "engines": { @@ -3408,8 +2335,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", - "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", "dev": true, "license": "MIT", "dependencies": { @@ -3436,8 +2361,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3446,8 +2369,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -3462,8 +2383,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -3475,8 +2394,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", - "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", "dev": true, "license": "MIT", "dependencies": { @@ -3499,8 +2416,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", - "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", "dev": true, "license": "MIT", "dependencies": { @@ -3515,192 +2430,8 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", "cpu": [ "x64" ], @@ -3713,8 +2444,6 @@ }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", "cpu": [ "x64" ], @@ -3725,69 +2454,8 @@ "linux" ] }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "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" @@ -3798,8 +2466,6 @@ }, "node_modules/acorn": { "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -3811,8 +2477,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3821,8 +2485,6 @@ }, "node_modules/ajv": { "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -3838,8 +2500,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3854,15 +2514,11 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3871,8 +2527,6 @@ }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { @@ -3888,8 +2542,6 @@ }, "node_modules/array-includes": { "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3911,8 +2563,6 @@ }, "node_modules/array.prototype.findlast": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3932,8 +2582,6 @@ }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3954,8 +2602,6 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { @@ -3973,8 +2619,6 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -3992,8 +2636,6 @@ }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "license": "MIT", "dependencies": { @@ -4009,8 +2651,6 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4031,15 +2671,11 @@ }, "node_modules/ast-types-flow": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true, "license": "MIT" }, "node_modules/async-function": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { @@ -4048,8 +2684,6 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4064,8 +2698,6 @@ }, "node_modules/axe-core": { "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "dev": true, "license": "MPL-2.0", "engines": { @@ -4074,8 +2706,6 @@ }, "node_modules/axobject-query": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4084,15 +2714,11 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "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", @@ -4110,19 +2736,20 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", - "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "version": "2.10.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz", + "integrity": "sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bcrypt": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -4135,8 +2762,6 @@ }, "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", @@ -4147,15 +2772,11 @@ }, "node_modules/bowser": { "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", - "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -4165,8 +2786,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -4178,8 +2797,6 @@ }, "node_modules/broker-factory": { "version": "3.1.12", - "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.12.tgz", - "integrity": "sha512-5Bmeki5j2IVO+lE07dSOUMZp1ZGKkE47b3ILv4ZD0nmTdc0iTKVS1CgYPDCy5m0Qb9jIKHBaF9SUrtqg5oW+1A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -4190,8 +2807,6 @@ }, "node_modules/browserslist": { "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -4224,8 +2839,6 @@ }, "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", @@ -4248,14 +2861,10 @@ }, "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": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", - "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4283,8 +2892,6 @@ }, "node_modules/c12/node_modules/dotenv": { "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "devOptional": true, "license": "BSD-2-Clause", "engines": { @@ -4296,8 +2903,6 @@ }, "node_modules/call-bind": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { @@ -4315,8 +2920,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4329,8 +2932,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -4346,8 +2947,6 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -4356,8 +2955,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001764", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", - "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", "funding": [ { "type": "opencollective", @@ -4376,8 +2973,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -4393,8 +2988,6 @@ }, "node_modules/chokidar": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4409,8 +3002,6 @@ }, "node_modules/citty": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4419,14 +3010,10 @@ }, "node_modules/client-only": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -4434,8 +3021,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4447,28 +3032,20 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "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": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "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" ], @@ -4482,8 +3059,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", @@ -4496,15 +3071,11 @@ }, "node_modules/confbox": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "devOptional": true, "license": "MIT" }, "node_modules/consola": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "devOptional": true, "license": "MIT", "engines": { @@ -4513,15 +3084,11 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -4535,15 +3102,11 @@ }, "node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -4554,8 +3117,6 @@ }, "node_modules/d3-color": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { "node": ">=12" @@ -4563,8 +3124,6 @@ }, "node_modules/d3-ease": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "engines": { "node": ">=12" @@ -4572,8 +3131,6 @@ }, "node_modules/d3-format": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", "license": "ISC", "engines": { "node": ">=12" @@ -4581,8 +3138,6 @@ }, "node_modules/d3-interpolate": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -4593,8 +3148,6 @@ }, "node_modules/d3-path": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "license": "ISC", "engines": { "node": ">=12" @@ -4602,8 +3155,6 @@ }, "node_modules/d3-scale": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -4618,8 +3169,6 @@ }, "node_modules/d3-shape": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -4630,8 +3179,6 @@ }, "node_modules/d3-time": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -4642,8 +3189,6 @@ }, "node_modules/d3-time-format": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -4654,8 +3199,6 @@ }, "node_modules/d3-timer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "engines": { "node": ">=12" @@ -4663,15 +3206,11 @@ }, "node_modules/damerau-levenshtein": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/data-view-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4688,8 +3227,6 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4706,8 +3243,6 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4724,8 +3259,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4741,21 +3274,15 @@ }, "node_modules/decimal.js-light": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge-ts": { "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", "devOptional": true, "license": "BSD-3-Clause", "engines": { @@ -4764,8 +3291,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { @@ -4782,8 +3307,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -4800,22 +3323,16 @@ }, "node_modules/defu": { "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "devOptional": true, "license": "MIT" }, "node_modules/destr": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "devOptional": true, "license": "MIT" }, "node_modules/detect-libc": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "devOptional": true, "license": "Apache-2.0", "engines": { @@ -4824,8 +3341,6 @@ }, "node_modules/doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4837,8 +3352,6 @@ }, "node_modules/dotenv": { "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4850,8 +3363,6 @@ }, "node_modules/dotenv-cli": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-11.0.0.tgz", - "integrity": "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==", "dev": true, "license": "MIT", "dependencies": { @@ -4866,8 +3377,6 @@ }, "node_modules/dotenv-expand": { "version": "12.0.3", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", - "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4882,8 +3391,6 @@ }, "node_modules/dotenv-expand/node_modules/dotenv": { "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4895,8 +3402,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -4910,8 +3415,6 @@ }, "node_modules/effect": { "version": "3.18.4", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", - "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4921,22 +3424,16 @@ }, "node_modules/electron-to-chromium": { "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, "node_modules/empathic": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", "devOptional": true, "license": "MIT", "engines": { @@ -4945,8 +3442,6 @@ }, "node_modules/enhanced-resolve": { "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4959,8 +3454,6 @@ }, "node_modules/es-abstract": { "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -5028,8 +3521,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -5038,8 +3529,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -5048,8 +3537,6 @@ }, "node_modules/es-iterator-helpers": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { @@ -5076,8 +3563,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -5089,8 +3574,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -5105,8 +3588,6 @@ }, "node_modules/es-shim-unscopables": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", "dependencies": { @@ -5118,8 +3599,6 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { @@ -5136,8 +3615,6 @@ }, "node_modules/es-toolkit": { "version": "1.43.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", - "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==", "license": "MIT", "workspaces": [ "docs", @@ -5146,8 +3623,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -5156,8 +3631,6 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -5169,8 +3642,6 @@ }, "node_modules/eslint": { "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { @@ -5229,8 +3700,6 @@ }, "node_modules/eslint-config-next": { "version": "16.0.10", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.10.tgz", - "integrity": "sha512-BxouZUm0I45K4yjOOIzj24nTi0H2cGo0y7xUmk+Po/PYtJXFBYVDS1BguE7t28efXjKdcN0tmiLivxQy//SsZg==", "dev": true, "license": "MIT", "dependencies": { @@ -5256,8 +3725,6 @@ }, "node_modules/eslint-config-next/node_modules/globals": { "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -5269,8 +3736,6 @@ }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "license": "MIT", "dependencies": { @@ -5281,8 +3746,6 @@ }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5291,8 +3754,6 @@ }, "node_modules/eslint-import-resolver-typescript": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", "dev": true, "license": "ISC", "dependencies": { @@ -5326,8 +3787,6 @@ }, "node_modules/eslint-module-utils": { "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -5344,8 +3803,6 @@ }, "node_modules/eslint-module-utils/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5354,8 +3811,6 @@ }, "node_modules/eslint-plugin-import": { "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { @@ -5388,8 +3843,6 @@ }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5398,8 +3851,6 @@ }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5428,8 +3879,6 @@ }, "node_modules/eslint-plugin-react": { "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -5461,8 +3910,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, "license": "MIT", "dependencies": { @@ -5481,8 +3928,6 @@ }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "license": "MIT", "dependencies": { @@ -5499,8 +3944,6 @@ }, "node_modules/eslint-scope": { "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5516,8 +3959,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5529,8 +3970,6 @@ }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5547,8 +3986,6 @@ }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5560,8 +3997,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5573,8 +4008,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5583,8 +4016,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5593,8 +4024,6 @@ }, "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" @@ -5602,14 +4031,10 @@ }, "node_modules/eventemitter3": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "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" @@ -5617,15 +4042,11 @@ }, "node_modules/exsolve": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "devOptional": true, "license": "MIT" }, "node_modules/fast-check": { "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", "devOptional": true, "funding": [ { @@ -5647,15 +4068,11 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "license": "MIT", "dependencies": { @@ -5671,8 +4088,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -5684,22 +4099,16 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-printf": { "version": "1.6.10", - "resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.10.tgz", - "integrity": "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==", "license": "BSD-3-Clause", "engines": { "node": ">=10.0" @@ -5707,8 +4116,6 @@ }, "node_modules/fast-unique-numbers": { "version": "9.0.25", - "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.25.tgz", - "integrity": "sha512-vHLSJfu0jSazb5X1jgYZIbsUd4mztxHxyFxUAPYvaYLkTsvQDn5+NbJRtfp+/tLIsUlMkD/geL2710QBxylH6w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -5720,8 +4127,6 @@ }, "node_modules/fast-xml-parser": { "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", "dev": true, "funding": [ { @@ -5739,8 +4144,6 @@ }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -5749,8 +4152,6 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5762,8 +4163,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -5775,8 +4174,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -5792,8 +4189,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -5806,15 +4201,11 @@ }, "node_modules/flatted": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, "node_modules/for-each": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { @@ -5829,8 +4220,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -5839,8 +4228,6 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5860,8 +4247,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", "funding": { @@ -5870,8 +4255,6 @@ }, "node_modules/generator-function": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", "dev": true, "license": "MIT", "engines": { @@ -5880,8 +4263,6 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { @@ -5890,8 +4271,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5915,8 +4294,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -5929,8 +4306,6 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { @@ -5947,8 +4322,6 @@ }, "node_modules/get-tsconfig": { "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5960,8 +4333,6 @@ }, "node_modules/giget": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", - "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5978,8 +4349,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -5991,8 +4360,6 @@ }, "node_modules/globals": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -6004,8 +4371,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6021,8 +4386,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -6034,15 +4397,11 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, "node_modules/has-bigints": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", "engines": { @@ -6054,8 +4413,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -6064,8 +4421,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { @@ -6077,8 +4432,6 @@ }, "node_modules/has-proto": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6093,8 +4446,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -6106,8 +4457,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { @@ -6122,8 +4471,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6135,21 +4482,15 @@ }, "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": { "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, "license": "MIT" }, "node_modules/hermes-parser": { "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { @@ -6158,8 +4499,6 @@ }, "node_modules/i18n": { "version": "0.15.3", - "resolved": "https://registry.npmjs.org/i18n/-/i18n-0.15.3.tgz", - "integrity": "sha512-tW/AA5R4lJZLnd60Agcd0PfXB1C2G7UqTrdNewuv/SIYdxcHkCE8w4Zx1SgCjJ+2BLuAAGIG/KXb/xNYF1lO5Q==", "license": "MIT", "dependencies": { "@messageformat/core": "^3.4.0", @@ -6178,8 +4517,6 @@ }, "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", @@ -6198,8 +4535,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -6208,8 +4543,6 @@ }, "node_modules/immer": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", "funding": { "type": "opencollective", @@ -6218,8 +4551,6 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6235,8 +4566,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -6245,14 +4574,10 @@ }, "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": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { @@ -6266,8 +4591,6 @@ }, "node_modules/internmap": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "license": "ISC", "engines": { "node": ">=12" @@ -6275,8 +4598,6 @@ }, "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" @@ -6284,8 +4605,6 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { @@ -6302,8 +4621,6 @@ }, "node_modules/is-async-function": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6322,8 +4639,6 @@ }, "node_modules/is-bigint": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6338,8 +4653,6 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { @@ -6355,8 +4668,6 @@ }, "node_modules/is-bun-module": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6365,8 +4676,6 @@ }, "node_modules/is-bun-module/node_modules/semver": { "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -6378,8 +4687,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { @@ -6391,8 +4698,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -6407,8 +4712,6 @@ }, "node_modules/is-data-view": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { @@ -6425,8 +4728,6 @@ }, "node_modules/is-date-object": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { @@ -6442,8 +4743,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -6452,8 +4751,6 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { @@ -6468,8 +4765,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { @@ -6488,8 +4783,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -6501,8 +4794,6 @@ }, "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { @@ -6514,8 +4805,6 @@ }, "node_modules/is-negative-zero": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", "engines": { @@ -6527,8 +4816,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -6537,8 +4824,6 @@ }, "node_modules/is-number-object": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -6554,8 +4839,6 @@ }, "node_modules/is-regex": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { @@ -6573,8 +4856,6 @@ }, "node_modules/is-set": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", "engines": { @@ -6586,8 +4867,6 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { @@ -6602,8 +4881,6 @@ }, "node_modules/is-string": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { @@ -6619,8 +4896,6 @@ }, "node_modules/is-symbol": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6637,8 +4912,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6653,8 +4926,6 @@ }, "node_modules/is-weakmap": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", "engines": { @@ -6666,8 +4937,6 @@ }, "node_modules/is-weakref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { @@ -6682,8 +4951,6 @@ }, "node_modules/is-weakset": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6699,22 +4966,16 @@ }, "node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { @@ -6731,8 +4992,6 @@ }, "node_modules/jiti": { "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", "bin": { @@ -6741,8 +5000,6 @@ }, "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", @@ -6751,15 +5008,11 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -6771,8 +5024,6 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -6784,29 +5035,21 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -6818,8 +5061,6 @@ }, "node_modules/jsx-ast-utils": { "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6834,8 +5075,6 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -6844,15 +5083,11 @@ }, "node_modules/language-subtag-registry": { "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", "dev": true, "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, "license": "MIT", "dependencies": { @@ -6864,8 +5099,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6878,8 +5111,6 @@ }, "node_modules/lightningcss": { "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -6906,157 +5137,8 @@ "lightningcss-win32-x64-msvc": "1.30.2" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", "cpu": [ "x64" ], @@ -7076,8 +5158,6 @@ }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", "cpu": [ "x64" ], @@ -7095,52 +5175,8 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -7155,15 +5191,11 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7175,8 +5207,6 @@ }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { @@ -7185,8 +5215,6 @@ }, "node_modules/lucide-react": { "version": "0.561.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz", - "integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -7194,8 +5222,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7204,14 +5230,10 @@ }, "node_modules/make-plural": { "version": "7.5.0", - "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.5.0.tgz", - "integrity": "sha512-0booA+aVYyVFoR67JBHdfVk0U08HmrBH2FrtmBqBa+NldlqXv/G2Z9VQuQq6Wgp2jDWdybEWGfBkk1cq5264WA==", "license": "Unicode-DFS-2016" }, "node_modules/math-interval-parser": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-2.0.1.tgz", - "integrity": "sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7219,8 +5241,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -7229,8 +5249,6 @@ }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -7239,8 +5257,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -7253,8 +5269,6 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -7266,8 +5280,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7275,14 +5287,10 @@ }, "node_modules/moo": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", - "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", "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", @@ -7313,8 +5321,6 @@ }, "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", @@ -7324,20 +5330,14 @@ }, "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": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/mustache": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "license": "MIT", "bin": { "mustache": "bin/mustache" @@ -7345,8 +5345,6 @@ }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -7363,8 +5361,6 @@ }, "node_modules/napi-postinstall": { "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, "license": "MIT", "bin": { @@ -7379,15 +5375,11 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/next": { "version": "16.0.10", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", - "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", "license": "MIT", "dependencies": { "@next/env": "16.0.10", @@ -7438,8 +5430,6 @@ }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -7466,8 +5456,6 @@ }, "node_modules/node-addon-api": { "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" @@ -7475,15 +5463,11 @@ }, "node_modules/node-fetch-native": { "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "devOptional": true, "license": "MIT" }, "node_modules/node-gyp-build": { "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -7493,15 +5477,11 @@ }, "node_modules/node-releases": { "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, "node_modules/nodemailer": { "version": "7.0.12", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", - "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -7509,8 +5489,6 @@ }, "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", @@ -7519,8 +5497,6 @@ }, "node_modules/nypm": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -7539,8 +5515,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", "engines": { @@ -7549,8 +5523,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -7562,8 +5534,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "engines": { @@ -7572,8 +5542,6 @@ }, "node_modules/object.assign": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { @@ -7593,8 +5561,6 @@ }, "node_modules/object.entries": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -7609,8 +5575,6 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7628,8 +5592,6 @@ }, "node_modules/object.groupby": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7643,8 +5605,6 @@ }, "node_modules/object.values": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -7662,15 +5622,11 @@ }, "node_modules/ohash": { "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "devOptional": true, "license": "MIT" }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -7687,8 +5643,6 @@ }, "node_modules/own-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", "dependencies": { @@ -7705,8 +5659,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7721,8 +5673,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -7737,8 +5687,6 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -7750,8 +5698,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -7760,8 +5706,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -7770,35 +5714,25 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "devOptional": true, "license": "MIT" }, "node_modules/perfect-debounce": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "devOptional": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -7810,8 +5744,6 @@ }, "node_modules/pkg-types": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", "devOptional": true, "license": "MIT", "dependencies": { @@ -7822,8 +5754,6 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { @@ -7832,8 +5762,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -7861,8 +5789,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -7871,8 +5797,6 @@ }, "node_modules/prisma": { "version": "6.19.2", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", - "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -7897,8 +5821,6 @@ }, "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" @@ -7906,14 +5828,10 @@ }, "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": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, "license": "MIT", "dependencies": { @@ -7924,15 +5842,11 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true, "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -7941,8 +5855,6 @@ }, "node_modules/pure-rand": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "devOptional": true, "funding": [ { @@ -7958,8 +5870,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -7979,8 +5889,6 @@ }, "node_modules/rc9": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -7990,8 +5898,6 @@ }, "node_modules/react": { "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", - "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7999,8 +5905,6 @@ }, "node_modules/react-dom": { "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -8011,15 +5915,11 @@ }, "node_modules/react-is": { "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", - "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", "license": "MIT", "peer": true }, "node_modules/react-redux": { "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", "dependencies": { "@types/use-sync-external-store": "^0.0.6", @@ -8041,8 +5941,6 @@ }, "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", @@ -8057,8 +5955,6 @@ }, "node_modules/readdirp": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "devOptional": true, "license": "MIT", "engines": { @@ -8071,8 +5967,6 @@ }, "node_modules/recharts": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", - "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", "license": "MIT", "workspaces": [ "www" @@ -8101,14 +5995,10 @@ }, "node_modules/redux": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "license": "MIT", "peerDependencies": { "redux": "^5.0.0" @@ -8116,8 +6006,6 @@ }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { @@ -8139,8 +6027,6 @@ }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { @@ -8160,14 +6046,10 @@ }, "node_modules/reselect": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "license": "MIT" }, "node_modules/resolve": { "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8187,8 +6069,6 @@ }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -8197,8 +6077,6 @@ }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", "funding": { @@ -8207,8 +6085,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -8218,14 +6094,10 @@ }, "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": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -8248,8 +6120,6 @@ }, "node_modules/safe-array-concat": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8268,8 +6138,6 @@ }, "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", @@ -8288,14 +6156,10 @@ }, "node_modules/safe-identifier": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", - "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", "license": "ISC" }, "node_modules/safe-push-apply": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", "dependencies": { @@ -8311,8 +6175,6 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", "dependencies": { @@ -8329,14 +6191,10 @@ }, "node_modules/scheduler": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -8345,8 +6203,6 @@ }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -8363,8 +6219,6 @@ }, "node_modules/set-function-name": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8379,8 +6233,6 @@ }, "node_modules/set-proto": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", "dependencies": { @@ -8394,8 +6246,6 @@ }, "node_modules/sharp": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, @@ -8439,8 +6289,6 @@ }, "node_modules/sharp/node_modules/semver": { "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "optional": true, "bin": { @@ -8452,8 +6300,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -8465,8 +6311,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -8475,8 +6319,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { @@ -8495,8 +6337,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", "dependencies": { @@ -8512,8 +6352,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { @@ -8531,8 +6369,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { @@ -8551,8 +6387,6 @@ }, "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", @@ -8561,8 +6395,6 @@ }, "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", @@ -8575,8 +6407,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8584,8 +6414,6 @@ }, "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" @@ -8593,15 +6421,11 @@ }, "node_modules/stable-hash": { "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", "dev": true, "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8614,8 +6438,6 @@ }, "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" @@ -8623,8 +6445,6 @@ }, "node_modules/string.prototype.includes": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -8638,8 +6458,6 @@ }, "node_modules/string.prototype.matchall": { "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "license": "MIT", "dependencies": { @@ -8666,8 +6484,6 @@ }, "node_modules/string.prototype.repeat": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", "dependencies": { @@ -8677,8 +6493,6 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { @@ -8699,8 +6513,6 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8718,8 +6530,6 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { @@ -8736,8 +6546,6 @@ }, "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { @@ -8746,8 +6554,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -8759,8 +6565,6 @@ }, "node_modules/strnum": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "dev": true, "funding": [ { @@ -8772,8 +6576,6 @@ }, "node_modules/styled-jsx": { "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", "dependencies": { "client-only": "0.0.1" @@ -8795,8 +6597,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -8808,8 +6608,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { @@ -8821,15 +6619,11 @@ }, "node_modules/tailwindcss": { "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "dev": true, "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { @@ -8842,14 +6636,10 @@ }, "node_modules/tiny-invariant": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, "node_modules/tinyexec": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "devOptional": true, "license": "MIT", "engines": { @@ -8858,8 +6648,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8875,8 +6663,6 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -8893,8 +6679,6 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -8906,8 +6690,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8919,8 +6701,6 @@ }, "node_modules/ts-api-utils": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -8932,8 +6712,6 @@ }, "node_modules/tsconfig-paths": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "license": "MIT", "dependencies": { @@ -8945,8 +6723,6 @@ }, "node_modules/tsconfig-paths/node_modules/json5": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "license": "MIT", "dependencies": { @@ -8958,14 +6734,10 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -8977,8 +6749,6 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { @@ -8992,8 +6762,6 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { @@ -9012,8 +6780,6 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9034,8 +6800,6 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -9055,14 +6819,10 @@ }, "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": { "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -9075,8 +6835,6 @@ }, "node_modules/typescript-eslint": { "version": "8.53.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz", - "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==", "dev": true, "license": "MIT", "dependencies": { @@ -9099,8 +6857,6 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { @@ -9118,14 +6874,10 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unrs-resolver": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9159,8 +6911,6 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -9190,8 +6940,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9200,8 +6948,6 @@ }, "node_modules/use-sync-external-store": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -9209,14 +6955,10 @@ }, "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": { "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -9237,8 +6979,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -9253,8 +6993,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { @@ -9273,8 +7011,6 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9301,8 +7037,6 @@ }, "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "license": "MIT", "dependencies": { @@ -9320,8 +7054,6 @@ }, "node_modules/which-typed-array": { "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { @@ -9342,8 +7074,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -9352,8 +7082,6 @@ }, "node_modules/worker-factory": { "version": "7.0.47", - "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.47.tgz", - "integrity": "sha512-Ga5U8n7hJqovn98nlFnbyuJj66s8dCU4QOQd0dU0bje7uvrGGhOFeKtsTdB3b6fO5BD93F88rHpkBCGzgGloKw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -9363,8 +7091,6 @@ }, "node_modules/worker-timers": { "version": "8.0.28", - "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.28.tgz", - "integrity": "sha512-+AuNePH2P/PuhQURf5I+SIGBty4dq2CzoQEB+bMXIQiPrYj3WhkUtIW2bSzeETFWyXJFUdQGsyFeZtit15LkOw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -9375,8 +7101,6 @@ }, "node_modules/worker-timers-broker": { "version": "8.0.14", - "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.14.tgz", - "integrity": "sha512-ooCGGWGcAYbWEJY2nkA60K9mZ33atvg/QIOBJ3OzdQJU5Z7/NdPFlEiMLiCYW8dpeP/qLcsaUsZzETrKNgGicg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -9388,8 +7112,6 @@ }, "node_modules/worker-timers-worker": { "version": "9.0.12", - "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.12.tgz", - "integrity": "sha512-NBXCnKB/9CkhjWZz2dITgK94QM5GIJx+7LAlCA8mKeO6whdwmfH9S3iPEwakhn3+NOB9nHE3jQqdpKpZZJI23g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -9399,8 +7121,6 @@ }, "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" @@ -9432,15 +7152,11 @@ }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -9452,8 +7168,6 @@ }, "node_modules/zod": { "version": "4.3.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -9461,8 +7175,6 @@ }, "node_modules/zod-validation-error": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", "dev": true, "license": "MIT", "engines": { @@ -9471,6 +7183,96 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz", + "integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz", + "integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz", + "integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz", + "integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz", + "integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz", + "integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/package.json b/package.json index d6c930c..49af182 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/nodemailer": "^7.0.4", "@types/react": "^19", "@types/react-dom": "^19", + "baseline-browser-mapping": "^2.10.22", "dotenv-cli": "^11.0.0", "eslint": "^9", "eslint-config-next": "16.0.10", diff --git a/prisma/migrations/20260424200000_machine_cycle_unique_org_machine_ts_cycle/migration.sql b/prisma/migrations/20260424200000_machine_cycle_unique_org_machine_ts_cycle/migration.sql new file mode 100644 index 0000000..fe49b25 --- /dev/null +++ b/prisma/migrations/20260424200000_machine_cycle_unique_org_machine_ts_cycle/migration.sql @@ -0,0 +1,18 @@ +-- Dedupe existing rows (keep oldest by createdAt, then id) before unique constraint. +WITH ranked AS ( + SELECT + "id", + ROW_NUMBER() OVER ( + PARTITION BY "orgId", "machineId", "ts", "cycleCount" + ORDER BY "createdAt" ASC, "id" ASC + ) AS rn + FROM "MachineCycle" +) +DELETE FROM "MachineCycle" mc +USING ranked r +WHERE mc."id" = r."id" + AND r.rn > 1; + +-- One row per (org, machine, device ts, cycle counter) — blocks retry / fan-out duplicates. +CREATE UNIQUE INDEX "MachineCycle_orgId_machineId_ts_cycleCount_key" + ON "MachineCycle" ("orgId", "machineId", "ts", "cycleCount"); diff --git a/prisma/migrations/20260424210000_kpi_heartbeat_unique_org_machine_ts/migration.sql b/prisma/migrations/20260424210000_kpi_heartbeat_unique_org_machine_ts/migration.sql new file mode 100644 index 0000000..a3316d8 --- /dev/null +++ b/prisma/migrations/20260424210000_kpi_heartbeat_unique_org_machine_ts/migration.sql @@ -0,0 +1,35 @@ +-- Heartbeat: same device ts + machine = one row (retries / double POST). +WITH ranked_hb AS ( + SELECT + "id", + ROW_NUMBER() OVER ( + PARTITION BY "orgId", "machineId", "ts" + ORDER BY "ts_server" ASC, "id" ASC + ) AS rn + FROM "MachineHeartbeat" +) +DELETE FROM "MachineHeartbeat" h +USING ranked_hb r +WHERE h."id" = r."id" + AND r.rn > 1; + +CREATE UNIQUE INDEX "MachineHeartbeat_orgId_machineId_ts_key" + ON "MachineHeartbeat" ("orgId", "machineId", "ts"); + +-- KPI snapshot: same minute bucket (device ts) per machine — Node-RED aligns ts to minute. +WITH ranked_kpi AS ( + SELECT + "id", + ROW_NUMBER() OVER ( + PARTITION BY "orgId", "machineId", "ts" + ORDER BY "ts_server" ASC, "id" ASC + ) AS rn + FROM "MachineKpiSnapshot" +) +DELETE FROM "MachineKpiSnapshot" k +USING ranked_kpi r +WHERE k."id" = r."id" + AND r.rn > 1; + +CREATE UNIQUE INDEX "MachineKpiSnapshot_orgId_machineId_ts_key" + ON "MachineKpiSnapshot" ("orgId", "machineId", "ts"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a6d7bb3..1f2ae98 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,65 +8,61 @@ datasource db { } model Org { - id String @id @default(uuid()) - name String - slug String @unique - createdAt DateTime @default(now()) - - members OrgUser[] - sessions Session[] - machines Machine[] - heartbeats MachineHeartbeat[] - kpiSnapshots MachineKpiSnapshot[] - events MachineEvent[] - workOrders MachineWorkOrder[] - settings OrgSettings? - shifts OrgShift[] - machineSettings MachineSettings[] - settingsAudits SettingsAudit[] - invites OrgInvite[] - alertPolicies AlertPolicy[] - alertContacts AlertContact[] - alertNotifications AlertNotification[] - financialProfile OrgFinancialProfile? + id String @id @default(uuid()) + name String + slug String @unique + createdAt DateTime @default(now()) + machines Machine[] + events MachineEvent[] + heartbeats MachineHeartbeat[] + kpiSnapshots MachineKpiSnapshot[] + members OrgUser[] + reasonEntries ReasonEntry[] + sessions Session[] + alertContacts AlertContact[] + alertNotifications AlertNotification[] + alertPolicies AlertPolicy? + downtimeActions DowntimeAction[] locationFinancialOverrides LocationFinancialOverride[] - machineFinancialOverrides MachineFinancialOverride[] - productCostOverrides ProductCostOverride[] - reasonEntries ReasonEntry[] - downtimeActions DowntimeAction[] - + machineFinancialOverrides MachineFinancialOverride[] + machineSettings MachineSettings[] + workOrders MachineWorkOrder[] + financialProfile OrgFinancialProfile? + invites OrgInvite[] + settings OrgSettings? + shifts OrgShift[] + productCostOverrides ProductCostOverride[] + settingsAudits SettingsAudit[] } model User { - id String @id @default(uuid()) - email String @unique - name String? - phone String? @map("phone") - passwordHash String - isActive Boolean @default(true) - createdAt DateTime @default(now()) - emailVerifiedAt DateTime? @map("email_verified_at") - emailVerificationToken String? @unique @map("email_verification_token") - emailVerificationExpiresAt DateTime? @map("email_verification_expires_at") - - orgs OrgUser[] - sessions Session[] - sentInvites OrgInvite[] @relation("OrgInviteInviter") - alertContacts AlertContact[] - alertNotifications AlertNotification[] - downtimeActionsOwned DowntimeAction[] @relation("DowntimeActionOwner") - downtimeActionsCreated DowntimeAction[] @relation("DowntimeActionCreator") + id String @id @default(uuid()) + email String @unique + name String? + passwordHash String + isActive Boolean @default(true) + createdAt DateTime @default(now()) + emailVerificationExpiresAt DateTime? @map("email_verification_expires_at") + emailVerificationToken String? @unique @map("email_verification_token") + emailVerifiedAt DateTime? @map("email_verified_at") + phone String? @map("phone") + orgs OrgUser[] + sessions Session[] + alertContacts AlertContact[] + alertNotifications AlertNotification[] + downtimeActionsCreated DowntimeAction[] @relation("DowntimeActionCreator") + downtimeActionsOwned DowntimeAction[] @relation("DowntimeActionOwner") + sentInvites OrgInvite[] @relation("OrgInviteInviter") } model OrgUser { id String @id @default(uuid()) orgId String userId String - role String @default("MEMBER") // OWNER | ADMIN | MEMBER + role String @default("MEMBER") createdAt DateTime @default(now()) - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([orgId, userId]) @@index([userId]) @@ -74,19 +70,18 @@ model OrgUser { } model OrgInvite { - id String @id @default(uuid()) - orgId String @map("org_id") + id String @id @default(uuid()) + orgId String @map("org_id") email String - role String @default("MEMBER") // OWNER | ADMIN | MEMBER - token String @unique - invitedBy String? @map("invited_by") - createdAt DateTime @default(now()) @map("created_at") - expiresAt DateTime @map("expires_at") + role String @default("MEMBER") + token String @unique + invitedBy String? @map("invited_by") + createdAt DateTime @default(now()) @map("created_at") + expiresAt DateTime @map("expires_at") acceptedAt DateTime? @map("accepted_at") revokedAt DateTime? @map("revoked_at") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - inviter User? @relation("OrgInviteInviter", fields: [invitedBy], references: [id], onDelete: SetNull) + inviter User? @relation("OrgInviteInviter", fields: [invitedBy], references: [id]) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@index([orgId]) @@index([orgId, email]) @@ -95,7 +90,7 @@ model OrgInvite { } model Session { - id String @id @default(uuid()) // cookie value + id String @id @default(uuid()) orgId String userId String createdAt DateTime @default(now()) @@ -104,9 +99,8 @@ model Session { revokedAt DateTime? ip String? userAgent String? - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([orgId]) @@ -114,39 +108,36 @@ model Session { } model Machine { - id String @id @default(uuid()) - orgId String - name String - apiKey String? @unique - code String? - location String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - tsDevice DateTime @default(now()) @map("ts") - tsServer DateTime @default(now()) @map("ts_server") - schemaVersion String? @map("schema_version") - seq BigInt? @map("seq") - pairingCode String? @unique @map("pairing_code") - pairingCodeExpiresAt DateTime? @map("pairing_code_expires_at") - pairingCodeUsedAt DateTime? @map("pairing_code_used_at") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - heartbeats MachineHeartbeat[] - kpiSnapshots MachineKpiSnapshot[] - events MachineEvent[] - cycles MachineCycle[] - workOrders MachineWorkOrder[] - settings MachineSettings? - settingsAudits SettingsAudit[] - alertNotifications AlertNotification[] - financialOverrides MachineFinancialOverride[] - reasonEntries ReasonEntry[] - downtimeActions DowntimeAction[] - + id String @id @default(uuid()) + orgId String + name String + code String? + createdAt DateTime @default(now()) + location String? + updatedAt DateTime @updatedAt + apiKey String? @unique + schemaVersion String? @map("schema_version") + seq BigInt? @map("seq") + tsDevice DateTime @default(now()) @map("ts") + tsServer DateTime @default(now()) @map("ts_server") + pairingCode String? @unique @map("pairing_code") + pairingCodeExpiresAt DateTime? @map("pairing_code_expires_at") + pairingCodeUsedAt DateTime? @map("pairing_code_used_at") + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + cycles MachineCycle[] + events MachineEvent[] + heartbeats MachineHeartbeat[] + kpiSnapshots MachineKpiSnapshot[] + reasonEntries ReasonEntry[] + alertNotifications AlertNotification[] + downtimeActions DowntimeAction[] + financialOverrides MachineFinancialOverride[] + settings MachineSettings? + workOrders MachineWorkOrder[] + settingsAudits SettingsAudit[] @@unique([orgId, name]) @@index([orgId]) - @@index([orgId, createdAt]) } model MachineHeartbeat { @@ -154,111 +145,97 @@ model MachineHeartbeat { orgId String machineId String ts DateTime @default(now()) - tsServer DateTime @default(now()) @map("ts_server") + status String + message String? + ip String? + fwVersion String? schemaVersion String? @map("schema_version") seq BigInt? @map("seq") - - status String - message String? - ip String? - fwVersion String? - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + tsServer DateTime @default(now()) @map("ts_server") + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@index([orgId, machineId, ts]) - @@index([orgId, machineId, tsServer]) } model MachineKpiSnapshot { - id String @id @default(uuid()) - orgId String - machineId String - ts DateTime @default(now()) - - workOrderId String? - sku String? - - target Int? - good Int? - scrap Int? - cycleCount Int? - goodParts Int? - scrapParts Int? - cavities Int? - cycleTime Float? // theoretical/target - actualCycle Float? // if you want (optional) - - availability Float? - performance Float? - quality Float? - oee Float? - + id String @id @default(uuid()) + orgId String + machineId String + ts DateTime @default(now()) + workOrderId String? + sku String? + target Int? + good Int? + scrap Int? + cycleCount Int? + goodParts Int? + scrapParts Int? + cavities Int? + cycleTime Float? + actualCycle Float? + availability Float? + performance Float? + quality Float? + oee Float? trackingEnabled Boolean? productionStarted Boolean? - tsServer DateTime @default(now()) @map("ts_server") schemaVersion String? @map("schema_version") seq BigInt? @map("seq") + tsServer DateTime @default(now()) @map("ts_server") + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) - + @@unique([orgId, machineId, seq], map: "uq_kpi_org_machine_seq") @@index([orgId, machineId, ts]) } model MachineEvent { - id String @id @default(uuid()) - orgId String - machineId String - ts DateTime @default(now()) - - topic String // "anomaly-detected" - eventType String // "slow-cycle" - severity String // "critical" + id String @id @default(uuid()) + orgId String + machineId String + ts DateTime @default(now()) + topic String + eventType String + severity String requiresAck Boolean @default(false) title String description String? - tsServer DateTime @default(now()) @map("ts_server") + data Json? + workOrderId String? + sku String? schemaVersion String? @map("schema_version") seq BigInt? @map("seq") + tsServer DateTime @default(now()) @map("ts_server") + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - // store the raw data blob so we don't lose fields - data Json? - - workOrderId String? - sku String? - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) - + @@unique([orgId, machineId, seq], map: "uq_event_org_machine_seq") @@index([orgId, machineId, ts]) @@index([orgId, machineId, eventType, ts]) } model MachineCycle { - id String @id @default(uuid()) - orgId String - machineId String - ts DateTime @default(now()) - + id String @id @default(uuid()) + orgId String + machineId String + ts DateTime @default(now()) cycleCount Int? actualCycleTime Float theoreticalCycleTime Float? + workOrderId String? + sku String? + cavities Int? + goodDelta Int? + scrapDelta Int? + createdAt DateTime @default(now()) + schemaVersion String? @map("schema_version") + seq BigInt? @map("seq") + tsServer DateTime @default(now()) @map("ts_server") + machine Machine @relation(fields: [machineId], references: [id]) - workOrderId String? - sku String? - - cavities Int? - goodDelta Int? - scrapDelta Int? - tsServer DateTime @default(now()) @map("ts_server") - schemaVersion String? @map("schema_version") - seq BigInt? @map("seq") - - createdAt DateTime @default(now()) - - machine Machine @relation(fields: [machineId], references: [id]) - + @@unique([orgId, machineId, ts, cycleCount]) + @@unique([orgId, machineId, seq], map: "uq_cycle_org_machine_seq") @@index([orgId, machineId, ts]) @@index([orgId, machineId, cycleCount]) } @@ -272,14 +249,13 @@ model MachineWorkOrder { targetQty Int? cycleTime Float? status String @default("PENDING") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt goodParts Int @default(0) @map("good_parts") scrapParts Int @default(0) @map("scrap_parts") cycleCount Int @default(0) @map("cycle_count") - 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) + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@unique([machineId, workOrderId]) @@index([orgId, machineId]) @@ -296,14 +272,13 @@ model IngestLog { seq BigInt? tsDevice DateTime? tsServer DateTime @default(now()) - - ok Boolean - status Int - errorCode String? - errorMsg String? - body Json? - ip String? - userAgent String? + ok Boolean + status Int + errorCode String? + errorMsg String? + body Json? + ip String? + userAgent String? @@index([endpoint, tsServer]) @@index([machineId, tsServer]) @@ -311,44 +286,42 @@ model IngestLog { } model OrgSettings { - orgId String @id @map("org_id") - timezone String @default("UTC") - shiftChangeCompMin Int @default(10) @map("shift_change_comp_min") - lunchBreakMin Int @default(30) @map("lunch_break_min") - shiftScheduleOverridesJson Json? @map("shift_schedule_overrides_json") - stoppageMultiplier Float @default(1.5) @map("stoppage_multiplier") - 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") - qualitySpikeDeltaPct Float @default(5) @map("quality_spike_delta_pct") - alertsJson Json? @map("alerts_json") - defaultsJson Json? @map("defaults_json") - version Int @default(1) - updatedAt DateTime @updatedAt @map("updated_at") - updatedBy String? @map("updated_by") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + orgId String @id @map("org_id") + timezone String @default("UTC") + shiftChangeCompMin Int @default(10) @map("shift_change_comp_min") + lunchBreakMin Int @default(30) @map("lunch_break_min") + stoppageMultiplier Float @default(1.5) @map("stoppage_multiplier") + 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") + qualitySpikeDeltaPct Float @default(5) @map("quality_spike_delta_pct") + alertsJson Json? @map("alerts_json") + defaultsJson Json? @map("defaults_json") + version Int @default(1) + updatedAt DateTime @updatedAt @map("updated_at") + updatedBy String? @map("updated_by") + shiftScheduleOverridesJson Json? @map("shift_schedule_overrides_json") + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@map("org_settings") } model OrgFinancialProfile { - orgId String @id @map("org_id") - defaultCurrency String @default("USD") @map("default_currency") - machineCostPerMin Float? @map("machine_cost_per_min") - operatorCostPerMin Float? @map("operator_cost_per_min") - ratedRunningKw Float? @map("rated_running_kw") - idleKw Float? @map("idle_kw") - kwhRate Float? @map("kwh_rate") - energyMultiplier Float @default(1.0) @map("energy_multiplier") - energyCostPerMin Float? @map("energy_cost_per_min") - scrapCostPerUnit Float? @map("scrap_cost_per_unit") - rawMaterialCostPerUnit Float? @map("raw_material_cost_per_unit") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - updatedBy String? @map("updated_by") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + orgId String @id @map("org_id") + defaultCurrency String @default("USD") @map("default_currency") + machineCostPerMin Float? @map("machine_cost_per_min") + operatorCostPerMin Float? @map("operator_cost_per_min") + ratedRunningKw Float? @map("rated_running_kw") + idleKw Float? @map("idle_kw") + kwhRate Float? @map("kwh_rate") + energyMultiplier Float @default(1.0) @map("energy_multiplier") + energyCostPerMin Float? @map("energy_cost_per_min") + scrapCostPerUnit Float? @map("scrap_cost_per_unit") + rawMaterialCostPerUnit Float? @map("raw_material_cost_per_unit") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + updatedBy String? @map("updated_by") + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@map("org_financial_profiles") } @@ -370,8 +343,7 @@ model LocationFinancialOverride { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@unique([orgId, location]) @@index([orgId]) @@ -395,9 +367,8 @@ model MachineFinancialOverride { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@unique([orgId, machineId]) @@index([orgId]) @@ -405,16 +376,15 @@ model MachineFinancialOverride { } model ProductCostOverride { - id String @id @default(uuid()) - orgId String @map("org_id") - sku String - currency String? - rawMaterialCostPerUnit Float? @map("raw_material_cost_per_unit") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - updatedBy String? @map("updated_by") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + id String @id @default(uuid()) + orgId String @map("org_id") + sku String + currency String? + rawMaterialCostPerUnit Float? @map("raw_material_cost_per_unit") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + updatedBy String? @map("updated_by") + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@unique([orgId, sku]) @@index([orgId]) @@ -423,33 +393,30 @@ model ProductCostOverride { model AlertPolicy { id String @id @default(uuid()) - orgId String @map("org_id") + orgId String @unique @map("org_id") policyJson Json @map("policy_json") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - - @@unique([orgId]) @@index([orgId]) @@map("alert_policies") } model AlertContact { - id String @id @default(uuid()) - orgId String @map("org_id") - userId String? @map("user_id") - name String - roleScope String @map("role_scope") // MEMBER | ADMIN | OWNER | CUSTOM - email String? - phone String? - eventTypes Json? @map("event_types") // optional allowlist (array of strings) - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + id String @id @default(uuid()) + orgId String @map("org_id") + userId String? @map("user_id") + name String + roleScope String @map("role_scope") + email String? + phone String? + eventTypes Json? @map("event_types") + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id]) notifications AlertNotification[] @@unique([orgId, userId]) @@ -459,24 +426,23 @@ model AlertContact { } model AlertNotification { - id String @id @default(uuid()) - orgId String @map("org_id") - machineId String @map("machine_id") - eventId String @map("event_id") - eventType String @map("event_type") - ruleId String @map("rule_id") + id String @id @default(uuid()) + orgId String @map("org_id") + machineId String @map("machine_id") + eventId String @map("event_id") + eventType String @map("event_type") + ruleId String @map("rule_id") role String channel String - contactId String? @map("contact_id") - userId String? @map("user_id") - sentAt DateTime @default(now()) @map("sent_at") + contactId String? @map("contact_id") + userId String? @map("user_id") + sentAt DateTime @default(now()) @map("sent_at") status String error String? - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) - contact AlertContact? @relation(fields: [contactId], references: [id], onDelete: SetNull) - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + contact AlertContact? @relation(fields: [contactId], references: [id]) + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id]) @@index([orgId, machineId, sentAt]) @@index([orgId, eventId, role, channel]) @@ -493,8 +459,7 @@ model OrgShift { endTime String @map("end_time") sortOrder Int @map("sort_order") enabled Boolean @default(true) - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@index([orgId]) @@index([orgId, sortOrder]) @@ -507,9 +472,8 @@ model MachineSettings { overridesJson Json? @map("overrides_json") updatedAt DateTime @updatedAt @map("updated_at") updatedBy String? @map("updated_by") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@index([orgId]) @@map("machine_settings") @@ -523,9 +487,8 @@ model SettingsAudit { source String payloadJson Json @map("payload_json") createdAt DateTime @default(now()) @map("created_at") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - machine Machine? @relation(fields: [machineId], references: [id], onDelete: Cascade) + machine Machine? @relation(fields: [machineId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@index([orgId, createdAt]) @@index([machineId, createdAt]) @@ -533,75 +496,58 @@ model SettingsAudit { } model ReasonEntry { - id String @id @default(uuid()) - orgId String - machineId String - - // idempotency key from Edge (rsn_) - reasonId String @unique - - // "downtime" | "scrap" - kind String - - // For downtime reasons + id String @id @default(uuid()) + orgId String + machineId String + reasonId String @unique + kind String episodeId String? durationSeconds Int? episodeEndTs DateTime? + scrapEntryId String? + scrapQty Int? + scrapUnit String? + reasonCode String + reasonLabel String? + reasonText String? + capturedAt DateTime + workOrderId String? + meta Json? + schemaVersion Int @default(1) + createdAt DateTime @default(now()) + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - // For scrap reasons - scrapEntryId String? - scrapQty Int? - scrapUnit String? - - // Required reason - reasonCode String - reasonLabel String? - reasonText String? - - capturedAt DateTime - workOrderId String? - meta Json? - schemaVersion Int @default(1) - - createdAt DateTime @default(now()) - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) - - @@index([orgId, machineId, capturedAt]) - @@index([orgId, kind, capturedAt]) @@unique([orgId, kind, episodeId]) @@unique([orgId, kind, scrapEntryId]) + @@index([orgId, machineId, capturedAt]) + @@index([orgId, kind, capturedAt]) } model DowntimeAction { - id String @id @default(uuid()) - orgId String @map("org_id") - machineId String? @map("machine_id") - reasonCode String? @map("reason_code") - hmDay Int? @map("hm_day") - hmHour Int? @map("hm_hour") - - title String - notes String? - status String @default("open") - priority String @default("medium") - dueDate DateTime? @map("due_date") - reminderAt DateTime? @map("reminder_at") + id String @id @default(uuid()) + orgId String @map("org_id") + machineId String? @map("machine_id") + reasonCode String? @map("reason_code") + hmDay Int? @map("hm_day") + hmHour Int? @map("hm_hour") + title String + notes String? + status String @default("open") + priority String @default("medium") + dueDate DateTime? @map("due_date") + reminderAt DateTime? @map("reminder_at") lastReminderAt DateTime? @map("last_reminder_at") - reminderStage String? @map("reminder_stage") - completedAt DateTime? @map("completed_at") - - ownerUserId String? @map("owner_user_id") - createdBy String? @map("created_by") - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) - machine Machine? @relation(fields: [machineId], references: [id], onDelete: SetNull) - ownerUser User? @relation("DowntimeActionOwner", fields: [ownerUserId], references: [id], onDelete: SetNull) - creator User? @relation("DowntimeActionCreator", fields: [createdBy], references: [id], onDelete: SetNull) + completedAt DateTime? @map("completed_at") + ownerUserId String? @map("owner_user_id") + createdBy String? @map("created_by") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + reminderStage String? @map("reminder_stage") + creator User? @relation("DowntimeActionCreator", fields: [createdBy], references: [id]) + machine Machine? @relation(fields: [machineId], references: [id]) + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + ownerUser User? @relation("DowntimeActionOwner", fields: [ownerUserId], references: [id]) @@index([orgId]) @@index([orgId, machineId])