Macrostop and timeline segmentation
This commit is contained in:
@@ -130,7 +130,7 @@
|
||||
"machines.empty": "No machines found for this org.",
|
||||
"machines.status": "Status",
|
||||
"machines.status.noHeartbeat": "No heartbeat",
|
||||
"machines.status.ok": "OK",
|
||||
"machines.status.ok": "Heartbeat",
|
||||
"machines.status.offline": "OFFLINE",
|
||||
"machines.status.unknown": "UNKNOWN",
|
||||
"machines.lastSeen": "Last seen {time}",
|
||||
@@ -140,6 +140,14 @@
|
||||
"machine.detail.error.failed": "Failed to load machine",
|
||||
"machine.detail.error.network": "Network error",
|
||||
"machine.detail.back": "Back",
|
||||
"machine.detail.workOrders.upload": "Upload Work Orders",
|
||||
"machine.detail.workOrders.uploading": "Uploading...",
|
||||
"machine.detail.workOrders.uploadParsing": "Parsing file...",
|
||||
"machine.detail.workOrders.uploadHint": "CSV or XLSX with Work Order ID, SKU, Theoretical Cycle Time (Seconds), Target Quantity.",
|
||||
"machine.detail.workOrders.uploadSuccess": "Uploaded {count} work orders",
|
||||
"machine.detail.workOrders.uploadError": "Upload failed",
|
||||
"machine.detail.workOrders.uploadInvalid": "No valid work orders found",
|
||||
"machine.detail.workOrders.uploadUnauthorized": "Not authorized to upload work orders",
|
||||
"machine.detail.status.offline": "OFFLINE",
|
||||
"machine.detail.status.unknown": "UNKNOWN",
|
||||
"machine.detail.status.run": "RUN",
|
||||
@@ -286,6 +294,7 @@
|
||||
"settings.thresholds.performance": "Performance threshold",
|
||||
"settings.thresholds.qualitySpike": "Quality spike delta",
|
||||
"settings.thresholds.stoppage": "Stoppage multiplier",
|
||||
"settings.thresholds.macroStoppage": "Macro stoppage multiplier",
|
||||
"settings.alerts": "Alerts",
|
||||
"settings.alertsSubtitle": "Choose which alerts to notify.",
|
||||
"settings.alerts.oeeDrop": "OEE drop alerts",
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
"machines.empty": "No se encontraron máquinas para esta organización.",
|
||||
"machines.status": "Estado",
|
||||
"machines.status.noHeartbeat": "Sin heartbeat",
|
||||
"machines.status.ok": "OK",
|
||||
"machines.status.ok": "Latido",
|
||||
"machines.status.offline": "FUERA DE LÍNEA",
|
||||
"machines.status.unknown": "DESCONOCIDO",
|
||||
"machines.lastSeen": "Visto hace {time}",
|
||||
@@ -140,6 +140,14 @@
|
||||
"machine.detail.error.failed": "No se pudo cargar la máquina",
|
||||
"machine.detail.error.network": "Error de red",
|
||||
"machine.detail.back": "Volver",
|
||||
"machine.detail.workOrders.upload": "Subir ordenes de trabajo",
|
||||
"machine.detail.workOrders.uploading": "Subiendo...",
|
||||
"machine.detail.workOrders.uploadParsing": "Leyendo archivo...",
|
||||
"machine.detail.workOrders.uploadHint": "CSV o XLSX con Work Order ID, SKU, Theoretical Cycle Time (Seconds), Target Quantity.",
|
||||
"machine.detail.workOrders.uploadSuccess": "Se cargaron {count} ordenes de trabajo",
|
||||
"machine.detail.workOrders.uploadError": "No se pudo cargar",
|
||||
"machine.detail.workOrders.uploadInvalid": "No se encontraron ordenes de trabajo validas",
|
||||
"machine.detail.workOrders.uploadUnauthorized": "No autorizado para cargar ordenes de trabajo",
|
||||
"machine.detail.status.offline": "FUERA DE LÍNEA",
|
||||
"machine.detail.status.unknown": "DESCONOCIDO",
|
||||
"machine.detail.status.run": "EN MARCHA",
|
||||
@@ -286,6 +294,7 @@
|
||||
"settings.thresholds.performance": "Umbral de Performance",
|
||||
"settings.thresholds.qualitySpike": "Delta de pico de calidad",
|
||||
"settings.thresholds.stoppage": "Multiplicador de paro",
|
||||
"settings.thresholds.macroStoppage": "Multiplicador de macroparo",
|
||||
"settings.alerts": "Alertas",
|
||||
"settings.alertsSubtitle": "Elige qué alertas notificar.",
|
||||
"settings.alerts.oeeDrop": "Alertas por caída de OEE",
|
||||
|
||||
124
lib/mqtt.ts
Normal file
124
lib/mqtt.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import "server-only";
|
||||
|
||||
import mqtt, { MqttClient } from "mqtt";
|
||||
|
||||
type SettingsUpdate = {
|
||||
orgId: string;
|
||||
version: number;
|
||||
source?: string;
|
||||
updatedAt?: string;
|
||||
machineId?: string;
|
||||
overridesUpdatedAt?: string;
|
||||
};
|
||||
|
||||
type WorkOrdersUpdate = {
|
||||
orgId: string;
|
||||
machineId: string;
|
||||
count?: number;
|
||||
source?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
const MQTT_URL = process.env.MQTT_BROKER_URL || "";
|
||||
const MQTT_USERNAME = process.env.MQTT_USERNAME;
|
||||
const MQTT_PASSWORD = process.env.MQTT_PASSWORD;
|
||||
const MQTT_CLIENT_ID = process.env.MQTT_CLIENT_ID;
|
||||
const MQTT_TOPIC_PREFIX = (process.env.MQTT_TOPIC_PREFIX || "mis").replace(/\/+$/, "");
|
||||
const MQTT_QOS_RAW = Number(process.env.MQTT_QOS ?? "2");
|
||||
const MQTT_QOS = MQTT_QOS_RAW === 0 || MQTT_QOS_RAW === 1 || MQTT_QOS_RAW === 2 ? MQTT_QOS_RAW : 2;
|
||||
|
||||
let client: MqttClient | null = null;
|
||||
let connecting: Promise<MqttClient> | null = null;
|
||||
|
||||
function buildSettingsTopic(orgId: string, machineId?: string) {
|
||||
const base = `${MQTT_TOPIC_PREFIX}/org/${orgId}`;
|
||||
if (machineId) return `${base}/machines/${machineId}/settings/updated`;
|
||||
return `${base}/settings/updated`;
|
||||
}
|
||||
|
||||
function buildWorkOrdersTopic(orgId: string, machineId: string) {
|
||||
const base = `${MQTT_TOPIC_PREFIX}/org/${orgId}`;
|
||||
return `${base}/machines/${machineId}/work_orders/updated`;
|
||||
}
|
||||
|
||||
async function getClient() {
|
||||
if (!MQTT_URL) return null;
|
||||
if (client?.connected) return client;
|
||||
if (connecting) return connecting;
|
||||
|
||||
connecting = new Promise((resolve, reject) => {
|
||||
const next = mqtt.connect(MQTT_URL, {
|
||||
clientId: MQTT_CLIENT_ID,
|
||||
username: MQTT_USERNAME,
|
||||
password: MQTT_PASSWORD,
|
||||
clean: true,
|
||||
reconnectPeriod: 5000,
|
||||
});
|
||||
|
||||
next.once("connect", () => {
|
||||
client = next;
|
||||
connecting = null;
|
||||
resolve(next);
|
||||
});
|
||||
|
||||
next.once("error", (err) => {
|
||||
next.end(true);
|
||||
client = null;
|
||||
connecting = null;
|
||||
reject(err);
|
||||
});
|
||||
|
||||
next.once("close", () => {
|
||||
client = null;
|
||||
});
|
||||
});
|
||||
|
||||
return connecting;
|
||||
}
|
||||
|
||||
export async function publishSettingsUpdate(update: SettingsUpdate) {
|
||||
if (!MQTT_URL) return { ok: false, reason: "MQTT_NOT_CONFIGURED" as const };
|
||||
const mqttClient = await getClient();
|
||||
if (!mqttClient) return { ok: false, reason: "MQTT_NOT_CONFIGURED" as const };
|
||||
|
||||
const topic = buildSettingsTopic(update.orgId, update.machineId);
|
||||
const payload = JSON.stringify({
|
||||
type: update.machineId ? "machine_settings_updated" : "org_settings_updated",
|
||||
orgId: update.orgId,
|
||||
machineId: update.machineId,
|
||||
version: update.version,
|
||||
source: update.source || "control_tower",
|
||||
updatedAt: update.updatedAt,
|
||||
overridesUpdatedAt: update.overridesUpdatedAt,
|
||||
});
|
||||
|
||||
return new Promise<{ ok: true }>((resolve, reject) => {
|
||||
mqttClient.publish(topic, payload, { qos: MQTT_QOS }, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve({ ok: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function publishWorkOrdersUpdate(update: WorkOrdersUpdate) {
|
||||
if (!MQTT_URL) return { ok: false, reason: "MQTT_NOT_CONFIGURED" as const };
|
||||
const mqttClient = await getClient();
|
||||
if (!mqttClient) return { ok: false, reason: "MQTT_NOT_CONFIGURED" as const };
|
||||
|
||||
const topic = buildWorkOrdersTopic(update.orgId, update.machineId);
|
||||
const payload = JSON.stringify({
|
||||
type: "work_orders_updated",
|
||||
orgId: update.orgId,
|
||||
machineId: update.machineId,
|
||||
count: update.count ?? null,
|
||||
source: update.source || "control_tower",
|
||||
updatedAt: update.updatedAt,
|
||||
});
|
||||
|
||||
return new Promise<{ ok: true }>((resolve, reject) => {
|
||||
mqttClient.publish(topic, payload, { qos: MQTT_QOS }, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve({ ok: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -54,6 +54,7 @@ export function buildSettingsPayload(settings: any, shifts: any[]) {
|
||||
},
|
||||
thresholds: {
|
||||
stoppageMultiplier: settings.stoppageMultiplier,
|
||||
macroStoppageMultiplier: settings.macroStoppageMultiplier,
|
||||
oeeAlertThresholdPct: settings.oeeAlertThresholdPct,
|
||||
performanceThresholdPct: settings.performanceThresholdPct,
|
||||
qualitySpikeDeltaPct: settings.qualitySpikeDeltaPct,
|
||||
@@ -159,6 +160,14 @@ export function validateThresholds(thresholds: any) {
|
||||
}
|
||||
}
|
||||
|
||||
const macroStoppage = thresholds.macroStoppageMultiplier;
|
||||
if (macroStoppage != null) {
|
||||
const v = Number(macroStoppage);
|
||||
if (!Number.isFinite(v) || v < 1.1 || v > 20.0) {
|
||||
return { ok: false, error: "macroStoppageMultiplier must be 1.1-20.0" };
|
||||
}
|
||||
}
|
||||
|
||||
const oee = thresholds.oeeAlertThresholdPct;
|
||||
if (oee != null) {
|
||||
const v = Number(oee);
|
||||
|
||||
Reference in New Issue
Block a user