// ======================================== // SESSION STATE PERSISTENCE FUNCTIONS // ======================================== // Function 1: Sync Session State to Database // Usage: Call this from Machine cycles and Work Order buttons functions // Throttled to avoid database overload (updates every 5 seconds or on state changes) const syncSessionState = function(msg, node, forceUpdate = false) { const now = Date.now(); const lastSync = flow.get("lastSessionSync") || 0; // Throttle: only sync every 5 seconds unless forced if (!forceUpdate && (now - lastSync) < 5000) { return null; } flow.set("lastSessionSync", now); const sessionId = global.get("currentSessionId") || "session_" + Date.now(); global.set("currentSessionId", sessionId); const activeOrder = global.get("activeWorkOrder"); const activeOrderId = activeOrder ? activeOrder.id : null; const activeOrderData = activeOrder ? JSON.stringify(activeOrder) : null; const sessionData = { session_id: sessionId, production_start_time: global.get("productionStartTime") || null, operating_time: global.get("operatingTime") || 0, last_update_time: now, tracking_enabled: global.get("trackingEnabled") ? 1 : 0, cycle_count: global.get("cycleCount") || 0, active_work_order_id: activeOrderId, active_work_order_data: activeOrderData, machine_online: global.get("machineOnline") ? 1 : 0, production_started: global.get("productionStarted") ? 1 : 0, last_cycle_time: global.get("lastCycleTime") || null, last_machine_state: flow.get("lastMachineState") || 0, created_at: now, updated_at: now, is_active: 1 }; // Create upsert query (INSERT ... ON DUPLICATE KEY UPDATE) msg.topic = ` INSERT INTO session_state ( session_id, production_start_time, operating_time, last_update_time, tracking_enabled, cycle_count, active_work_order_id, active_work_order_data, machine_online, production_started, last_cycle_time, last_machine_state, created_at, updated_at, is_active ) VALUES ( '${sessionData.session_id}', ${sessionData.production_start_time}, ${sessionData.operating_time}, ${sessionData.last_update_time}, ${sessionData.tracking_enabled}, ${sessionData.cycle_count}, ${sessionData.active_work_order_id ? "'" + sessionData.active_work_order_id + "'" : "NULL"}, ${sessionData.active_work_order_data ? "'" + sessionData.active_work_order_data.replace(/'/g, "''") + "'" : "NULL"}, ${sessionData.machine_online}, ${sessionData.production_started}, ${sessionData.last_cycle_time}, ${sessionData.last_machine_state}, ${sessionData.created_at}, ${sessionData.updated_at}, ${sessionData.is_active} ) ON DUPLICATE KEY UPDATE production_start_time = VALUES(production_start_time), operating_time = VALUES(operating_time), last_update_time = VALUES(last_update_time), tracking_enabled = VALUES(tracking_enabled), cycle_count = VALUES(cycle_count), active_work_order_id = VALUES(active_work_order_id), active_work_order_data = VALUES(active_work_order_data), machine_online = VALUES(machine_online), production_started = VALUES(production_started), last_cycle_time = VALUES(last_cycle_time), last_machine_state = VALUES(last_machine_state), updated_at = VALUES(updated_at), is_active = VALUES(is_active); `; msg._mode = "session-sync"; msg.sessionData = sessionData; return msg; }; // ======================================== // Function 2: Startup Recovery // This function should run on Node-RED startup to check for and restore previous session const startupRecovery = function(msg, node) { // Query for most recent active session msg.topic = ` SELECT * FROM session_state WHERE is_active = 1 ORDER BY updated_at DESC LIMIT 1; `; msg._mode = "startup-recovery"; return msg; }; // ======================================== // Function 3: Process Recovery Data // This processes the database response from startup recovery query const processRecoveryData = function(msg, node) { if (!msg.payload || msg.payload.length === 0) { // No previous session found node.warn("No previous session found. Starting fresh."); msg._recoveryStatus = "no-session"; return msg; } const sessionData = msg.payload[0]; const now = Date.now(); const timeSinceLastUpdate = now - sessionData.last_update_time; const hoursElapsed = timeSinceLastUpdate / (1000 * 60 * 60); // If last update was more than 24 hours ago, consider it stale if (hoursElapsed > 24) { node.warn(`Previous session is stale (${hoursElapsed.toFixed(1)} hours old). Starting fresh.`); msg._recoveryStatus = "session-stale"; return msg; } // Prompt user for recovery msg._recoveryStatus = "session-found"; msg.recoveryData = { sessionId: sessionData.session_id, cycleCount: sessionData.cycle_count, operatingTime: sessionData.operating_time, workOrderId: sessionData.active_work_order_id, trackingEnabled: sessionData.tracking_enabled === 1, hoursElapsed: hoursElapsed.toFixed(1), lastUpdateTime: new Date(sessionData.last_update_time).toISOString() }; // Prepare prompt message for user msg.payload = { type: "recovery-prompt", message: `Previous session found from ${hoursElapsed.toFixed(1)} hours ago: - Work Order: ${sessionData.active_work_order_id || "None"} - Cycles completed: ${sessionData.cycle_count} - Operating time: ${(sessionData.operating_time / 3600).toFixed(2)} hours Would you like to restore this session?`, data: sessionData }; return msg; }; // ======================================== // Function 4: Restore Session // Call this when user confirms they want to restore the session const restoreSession = function(msg, node) { const sessionData = msg.payload; if (!sessionData) { node.error("No session data provided for restoration"); return null; } try { // Restore global context variables global.set("currentSessionId", sessionData.session_id); global.set("productionStartTime", sessionData.production_start_time); global.set("operatingTime", sessionData.operating_time || 0); global.set("trackingEnabled", sessionData.tracking_enabled === 1); global.set("cycleCount", sessionData.cycle_count || 0); global.set("machineOnline", sessionData.machine_online === 1); global.set("productionStarted", sessionData.production_started === 1); global.set("lastCycleTime", sessionData.last_cycle_time); // Restore flow context variables flow.set("lastMachineState", sessionData.last_machine_state || 0); // Restore active work order if exists if (sessionData.active_work_order_data) { try { const workOrder = JSON.parse(sessionData.active_work_order_data); global.set("activeWorkOrder", workOrder); } catch (e) { node.error("Failed to parse work order data: " + e.message); } } node.warn("Session restored successfully: " + sessionData.session_id); msg._recoveryStatus = "restored"; msg.payload = { success: true, message: "Session restored successfully", sessionId: sessionData.session_id }; } catch (error) { node.error("Error restoring session: " + error.message); msg._recoveryStatus = "restore-failed"; msg.payload = { success: false, message: "Failed to restore session: " + error.message }; } return msg; }; // ======================================== // Function 5: Deactivate Old Sessions // Call this when starting a new work order to mark old sessions as inactive const deactivateOldSessions = function(msg, node) { const currentSessionId = global.get("currentSessionId"); msg.topic = ` UPDATE session_state SET is_active = 0, updated_at = ${Date.now()} WHERE is_active = 1 ${currentSessionId ? "AND session_id != '" + currentSessionId + "'" : ""}; `; msg._mode = "deactivate-sessions"; return msg; }; // ======================================== // EXPORT for use in function nodes // ======================================== module.exports = { syncSessionState, startupRecovery, processRecoveryData, restoreSession, deactivateOldSessions };