File created successfully at: /home/mdares/.node-red/complete_optimization_migration.sql

-- ============================================================================
-- COMPLETE OPTIMIZATION DATABASE MIGRATION
-- Database: machine_data
-- Version: 2.0 - Full Implementation
-- Description: Creates all tables needed for KPI tracking optimization
-- ============================================================================

-- ----------------------------------------------------------------------------
-- Table 1: KPI Snapshots (Time-series data for graphs)
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS kpi_snapshots (
  id INT AUTO_INCREMENT PRIMARY KEY,
  timestamp BIGINT NOT NULL COMMENT 'Unix timestamp in milliseconds',
  work_order_id VARCHAR(255),
  oee_percent DECIMAL(5,2) DEFAULT 0,
  availability_percent DECIMAL(5,2) DEFAULT 0,
  performance_percent DECIMAL(5,2) DEFAULT 0,
  quality_percent DECIMAL(5,2) DEFAULT 0,
  cycle_count INT DEFAULT 0,
  good_parts INT DEFAULT 0,
  scrap_count INT DEFAULT 0,
  operating_time DECIMAL(10,2) DEFAULT 0 COMMENT 'Accumulated seconds in state 1',
  downtime DECIMAL(10,2) DEFAULT 0 COMMENT 'Accumulated seconds in state 0 while tracking',
  machine_state INT DEFAULT 0 COMMENT 'Current machine state: 0 or 1',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  INDEX idx_timestamp (timestamp),
  INDEX idx_work_order (work_order_id),
  INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KPI data snapshots for trending and graphs';

-- ----------------------------------------------------------------------------
-- Table 2: Alert History (Manual + Automatic alerts)
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS alert_history (
  id INT AUTO_INCREMENT PRIMARY KEY,
  timestamp BIGINT NOT NULL COMMENT 'Unix timestamp in milliseconds',
  alert_type VARCHAR(100) NOT NULL,
  description TEXT,
  severity VARCHAR(20) NOT NULL COMMENT 'info, warning, critical',
  source VARCHAR(50) NOT NULL COMMENT 'manual or automatic',
  work_order_id VARCHAR(255),
  acknowledged BOOLEAN DEFAULT 0,
  acknowledged_at BIGINT,
  acknowledged_by VARCHAR(100),
  auto_resolved BOOLEAN DEFAULT 0,
  resolved_at BIGINT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  INDEX idx_timestamp (timestamp),
  INDEX idx_severity (severity),
  INDEX idx_acknowledged (acknowledged),
  INDEX idx_work_order (work_order_id),
  INDEX idx_source (source)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Alert history for both manual and automatic alerts';

-- ----------------------------------------------------------------------------
-- Table 3: Shift Definitions (Reference data)
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS shift_definitions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  shift_name VARCHAR(50) NOT NULL,
  start_hour INT NOT NULL COMMENT '0-23',
  start_minute INT NOT NULL DEFAULT 0,
  end_hour INT NOT NULL COMMENT '0-23',
  end_minute INT NOT NULL DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  UNIQUE KEY unique_shift_name (shift_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Shift definitions for time range filtering';

-- Seed shift data
INSERT IGNORE INTO shift_definitions (id, shift_name, start_hour, start_minute, end_hour, end_minute) VALUES
(1, 'Day Shift', 6, 0, 15, 0),
(2, 'Evening Shift', 15, 0, 23, 0),
(3, 'Night Shift', 23, 0, 6, 0);

-- ----------------------------------------------------------------------------
-- Table 4: Session State (For crash recovery - Issue 1)
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS session_state (
  id INT AUTO_INCREMENT PRIMARY KEY,
  session_key VARCHAR(50) NOT NULL COMMENT 'Always "current_session" - single row table',
  work_order_id VARCHAR(255),
  cycle_count INT DEFAULT 0,
  production_start_time BIGINT COMMENT 'Unix timestamp when tracking started',
  operating_time DECIMAL(10,2) DEFAULT 0,
  downtime DECIMAL(10,2) DEFAULT 0,
  last_update_time BIGINT,
  tracking_enabled BOOLEAN DEFAULT 0,
  machine_state INT DEFAULT 0,
  scrap_prompt_issued_for VARCHAR(255),
  current_session_id VARCHAR(100) COMMENT 'Links to production_sessions',
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  UNIQUE KEY unique_session (session_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Current session state for crash recovery';

-- Initialize the single session row
INSERT IGNORE INTO session_state (session_key, work_order_id, cycle_count, tracking_enabled)
VALUES ('current_session', NULL, 0, 0);

-- ----------------------------------------------------------------------------
-- Table 5: Stop Events (Issue 4 - Intelligent Downtime Categorization)
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS stop_events (
  id INT AUTO_INCREMENT PRIMARY KEY,
  work_order_id VARCHAR(255),
  session_id VARCHAR(100) COMMENT 'Links to production_sessions',
  stop_time BIGINT NOT NULL COMMENT 'Unix timestamp when stop occurred',
  resume_time BIGINT COMMENT 'Unix timestamp when resumed (NULL if not resumed yet)',
  duration DECIMAL(10,2) COMMENT 'Duration in seconds (calculated when resumed)',
  reason_category VARCHAR(20) NOT NULL COMMENT 'planned or unplanned',
  reason_detail VARCHAR(100) NOT NULL COMMENT 'Specific reason from predefined list',
  affects_availability BOOLEAN NOT NULL COMMENT 'TRUE for unplanned, FALSE for planned',
  operator_notes TEXT COMMENT 'Additional notes from operator',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  INDEX idx_work_order (work_order_id),
  INDEX idx_session (session_id),
  INDEX idx_stop_time (stop_time),
  INDEX idx_category (reason_category)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tracks all stop events with categorization';

-- ----------------------------------------------------------------------------
-- Table 6: Production Sessions (Issue 5 - Session Management)
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS production_sessions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  session_id VARCHAR(100) UNIQUE NOT NULL COMMENT 'UUID for this session',
  work_order_id VARCHAR(255),
  start_time BIGINT NOT NULL COMMENT 'Unix timestamp when session started',
  end_time BIGINT COMMENT 'Unix timestamp when session ended',
  duration DECIMAL(10,2) COMMENT 'Duration in seconds',
  cycles_completed INT DEFAULT 0,
  reason_for_start VARCHAR(50) COMMENT 'initial_start, resume_after_planned, resume_after_unplanned',
  reason_for_end VARCHAR(50) COMMENT 'planned_stop, unplanned_stop, work_order_complete',
  operating_time DECIMAL(10,2) DEFAULT 0 COMMENT 'Time spent in production',
  downtime DECIMAL(10,2) DEFAULT 0 COMMENT 'Downtime during this session',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  INDEX idx_work_order (work_order_id),
  INDEX idx_session (session_id),
  INDEX idx_start_time (start_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tracks production sessions for pattern analysis';

-- ----------------------------------------------------------------------------
-- Table 7: Cycle Anomalies (Issue 3 - Hardware Irregularity Tracking)
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS cycle_anomalies (
  id INT AUTO_INCREMENT PRIMARY KEY,
  work_order_id VARCHAR(255),
  session_id VARCHAR(100),
  cycle_number INT NOT NULL,
  expected_time DECIMAL(10,2) COMMENT 'Expected cycle time in seconds',
  actual_time DECIMAL(10,2) COMMENT 'Actual cycle time in seconds',
  deviation_percent DECIMAL(5,2) COMMENT 'Percentage deviation from expected',
  anomaly_type VARCHAR(50) COMMENT 'slower, faster, irregular',
  timestamp BIGINT NOT NULL COMMENT 'Unix timestamp when anomaly occurred',
  notes TEXT COMMENT 'System-generated notes',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  INDEX idx_work_order (work_order_id),
  INDEX idx_session (session_id),
  INDEX idx_timestamp (timestamp),
  INDEX idx_anomaly_type (anomaly_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tracks cycle time anomalies for diagnostics';

-- ----------------------------------------------------------------------------
-- Table 8: Update work_orders table (add new columns)
-- ----------------------------------------------------------------------------
-- Add scrap_count column if it doesn't exist
SET @col_exists = 0;
SELECT COUNT(*) INTO @col_exists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'machine_data'
  AND TABLE_NAME = 'work_orders'
  AND COLUMN_NAME = 'scrap_count';

SET @query = IF(@col_exists = 0,
  'ALTER TABLE work_orders ADD COLUMN scrap_count INT DEFAULT 0 AFTER good_parts',
  'SELECT "Column scrap_count already exists" AS Info');
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- Add session tracking columns
SET @col_exists = 0;
SELECT COUNT(*) INTO @col_exists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'machine_data'
  AND TABLE_NAME = 'work_orders'
  AND COLUMN_NAME = 'total_sessions';

SET @query = IF(@col_exists = 0,
  'ALTER TABLE work_orders ADD COLUMN total_sessions INT DEFAULT 0',
  'SELECT "Column total_sessions already exists" AS Info');
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @col_exists = 0;
SELECT COUNT(*) INTO @col_exists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'machine_data'
  AND TABLE_NAME = 'work_orders'
  AND COLUMN_NAME = 'total_operating_time';

SET @query = IF(@col_exists = 0,
  'ALTER TABLE work_orders ADD COLUMN total_operating_time DECIMAL(10,2) DEFAULT 0',
  'SELECT "Column total_operating_time already exists" AS Info');
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @col_exists = 0;
SELECT COUNT(*) INTO @col_exists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'machine_data'
  AND TABLE_NAME = 'work_orders'
  AND COLUMN_NAME = 'total_downtime';

SET @query = IF(@col_exists = 0,
  'ALTER TABLE work_orders ADD COLUMN total_downtime DECIMAL(10,2) DEFAULT 0',
  'SELECT "Column total_downtime already exists" AS Info');
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @col_exists = 0;
SELECT COUNT(*) INTO @col_exists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'machine_data'
  AND TABLE_NAME = 'work_orders'
  AND COLUMN_NAME = 'avg_session_duration';

SET @query = IF(@col_exists = 0,
  'ALTER TABLE work_orders ADD COLUMN avg_session_duration DECIMAL(10,2) DEFAULT 0',
  'SELECT "Column avg_session_duration already exists" AS Info');
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- Rename legacy scrap_parts to scrap_count if it exists
SET @col_exists_legacy = 0;
SELECT COUNT(*) INTO @col_exists_legacy
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'machine_data'
  AND TABLE_NAME = 'work_orders'
  AND COLUMN_NAME = 'scrap_parts';

SET @query = IF(@col_exists_legacy > 0,
  'ALTER TABLE work_orders CHANGE scrap_parts scrap_count INT DEFAULT 0',
  'SELECT "No legacy scrap_parts column to rename" AS Info');
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- ----------------------------------------------------------------------------
-- Verification Queries (Run these to confirm migration success)
-- ----------------------------------------------------------------------------
-- SELECT COUNT(*) AS kpi_snapshots_count FROM kpi_snapshots;
-- SELECT COUNT(*) AS alert_history_count FROM alert_history;
-- SELECT * FROM shift_definitions;
-- SELECT * FROM session_state WHERE session_key = 'current_session';
-- SELECT COUNT(*) AS stop_events_count FROM stop_events;
-- SELECT COUNT(*) AS production_sessions_count FROM production_sessions;
-- SELECT COUNT(*) AS cycle_anomalies_count FROM cycle_anomalies;
-- SHOW COLUMNS FROM work_orders;

-- ============================================================================
-- END OF MIGRATION
-- ============================================================================


Good! Now let me update the Machine cycles function with all the time tracking and state backup logic from the phase1 file, plus add the additional features like cycle capping and anomaly tracking.


TodoWrite

Review current Node-RED implementation structure

Reviewing current Node-RED implementation structure

Check existing database schema and tables

Checking existing database schema and tables

Analyze current flow logic for KPI tracking

Analyzing current flow logic for KPI tracking

Identify gaps vs optimization requirements

Identifying gaps vs optimization requirements

Create complete database migration script

Creating complete database migration script

Create enhanced Machine cycles function

Creating enhanced Machine cycles function

Create enhanced Work Order buttons function

Creating enhanced Work Order buttons function

Create database state backup handler

Create stop reason UI prompt

Create startup recovery flow

Update flows.json with all changes

Create implementation guide document


Write

// ============================================================================
// ENHANCED "Machine cycles" Function - Complete Implementation
// Location: flows.json, node ID: 0d023d87a13bf56f
// Outputs: 4 (cycle update, state change, state backup, anomaly detection)
//
// Features Implemented:
// - Time tracking (operating time and downtime)
// - State backup to database (every 10 cycles)
// - Cycle count capping at 100
// - Cycle anomaly detection (Issue 3)
// - Session tracking integration
// ============================================================================

const current = Number(msg.payload) || 0;

// ============================================================================
// SECTION 1: TIME TRACKING (Must run BEFORE any early returns)
// ============================================================================
const now = Date.now();
const trackingEnabled = !!global.get("trackingEnabled");
const lastUpdate = global.get("lastUpdateTime") || now;
const deltaMs = now - lastUpdate;
const deltaSeconds = deltaMs / 1000;

// Track last cycle time for anomaly detection
const lastCycleTimestamp = global.get("lastCycleTimestamp") || now;
const cycleTimeMs = now - lastCycleTimestamp;
const cycleTimeSeconds = cycleTimeMs / 1000;

// Sanity check: Protect against clock skew (negative delta or >5 min gap)
if (deltaSeconds < 0 || deltaSeconds > 300) {
    node.warn(`[TIME] Abnormal delta: ${deltaSeconds.toFixed(2)}s - clock skew or restart detected, resetting timer`);
    global.set("lastUpdateTime", now);
    // Don't accumulate time, but continue processing
} else {
    // Normal delta, accumulate time if tracking is enabled
    if (trackingEnabled) {
        // Initialize timing vars if they don't exist (handles restart scenario)
        if (!global.get("productionStartTime")) {
            node.warn("[TIME] Production start time missing, initializing now");
            global.set("productionStartTime", now);
            global.set("operatingTime", 0);
            global.set("downtime", 0);
        }

        // Accumulate operating time when machine is running (state 1)
        if (current === 1) {
            const opTime = global.get("operatingTime") || 0;
            global.set("operatingTime", opTime + deltaSeconds);
        }
        // Accumulate downtime when machine is stopped (state 0) while tracking
        else if (current === 0) {
            const downTime = global.get("downtime") || 0;
            global.set("downtime", downTime + deltaSeconds);
        }
    }
    global.set("lastUpdateTime", now);
}

// ============================================================================
// SECTION 2: EXISTING CYCLE COUNTING LOGIC
// ============================================================================
let zeroStreak = flow.get("zeroStreak") || 0;
zeroStreak = current === 0 ? zeroStreak + 1 : 0;
flow.set("zeroStreak", zeroStreak);

const prev = flow.get("lastMachineState") ?? 0;
flow.set("lastMachineState", current);

global.set("machineOnline", true);   // force ONLINE for now

let productionRunning = !!global.get("productionStarted");
let stateChanged = false;

if (current === 1 && !productionRunning) {
    productionRunning = true;
    stateChanged = true;
} else if (current === 0 && zeroStreak >= 2 && productionRunning) {
    productionRunning = false;
    stateChanged = true;
}

global.set("productionStarted", productionRunning);

const stateMsg = stateChanged
    ? {
        _mode: "production-state",
        machineOnline: true,
        productionStarted: productionRunning
    }
    : null;

const activeOrder = global.get("activeWorkOrder");
const cavities = Number(global.get("moldActive") || 0);
if (!activeOrder || !activeOrder.id || cavities <= 0) {
    // We still want to pass along any state change even if there's no active WO.
    return [null, stateMsg, null, null];
}

// Check if tracking is enabled (START button clicked)
if (!trackingEnabled) {
    // Cycles are happening but we're not tracking them yet
    return [null, stateMsg, null, null];
}

// only count rising edges (0 -> 1) for production totals
if (prev === 1 || current !== 1) {
    return [null, stateMsg, null, null];
}

// ============================================================================
// SECTION 3: CYCLE COUNT WITH CAPPING (Issue 2)
// ============================================================================
let cycles = Number(global.get("cycleCount") || 0);

// Check if we've reached the 100 cycle cap
if (cycles >= 100) {
    node.warn("[CYCLE CAP] Maximum 100 cycles reached. Prompting for work order completion.");

    // Create alert message
    msg._mode = "cycle-cap-reached";
    msg.alert = {
        id: activeOrder.id,
        sku: activeOrder.sku || "",
        cycles: cycles,
        message: "Maximum 100 cycles reached. Please complete the work order or enter scrap parts."
    };

    return [null, msg, null, null];
}

cycles = cycles + 1;
global.set("cycleCount", cycles);
global.set("lastCycleTimestamp", now);

// Show warning when approaching cap
if (cycles >= 90 && cycles < 100) {
    node.warn(`[CYCLE WARNING] Approaching cycle cap: ${cycles}/100 cycles completed`);
}

// ============================================================================
// SECTION 4: CYCLE ANOMALY DETECTION (Issue 3)
// ============================================================================
let anomalyMsg = null;

// Get theoretical cycle time from work order
const theoreticalCycleTime = Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0);

if (theoreticalCycleTime > 0 && cycles > 1) { // Skip first cycle (no baseline)
    // Calculate rolling average (last 10 cycles)
    let cycleTimeHistory = flow.get("cycleTimeHistory") || [];
    cycleTimeHistory.push(cycleTimeSeconds);

    // Keep only last 10 cycles
    if (cycleTimeHistory.length > 10) {
        cycleTimeHistory = cycleTimeHistory.slice(-10);
    }
    flow.set("cycleTimeHistory", cycleTimeHistory);

    // Calculate average
    const avgCycleTime = cycleTimeHistory.reduce((a, b) => a + b, 0) / cycleTimeHistory.length;

    // Calculate deviation from theoretical time
    const deviation = ((cycleTimeSeconds - theoreticalCycleTime) / theoreticalCycleTime) * 100;

    // Flag if deviation is > 20%
    if (Math.abs(deviation) > 20) {
        const anomalyType = deviation > 0 ? "slower" : "faster";

        node.warn(`[ANOMALY] Cycle ${cycles}: ${cycleTimeSeconds.toFixed(2)}s (expected: ${theoreticalCycleTime}s, deviation: ${deviation.toFixed(1)}%)`);

        const sessionId = global.get("currentSessionId") || null;

        anomalyMsg = {
            _mode: "cycle-anomaly",
            topic: `
                INSERT INTO cycle_anomalies
                (work_order_id, session_id, cycle_number, expected_time, actual_time, deviation_percent, anomaly_type, timestamp, notes)
                VALUES
                ('${activeOrder.id}', ${sessionId ? `'${sessionId}'` : 'NULL'}, ${cycles}, ${theoreticalCycleTime}, ${cycleTimeSeconds.toFixed(2)}, ${deviation.toFixed(2)}, '${anomalyType}', ${now}, 'Automatic detection: ${deviation.toFixed(1)}% deviation from expected');
            `
        };
    }
}

// ============================================================================
// SECTION 5: GOOD PARTS CALCULATION
// ============================================================================
// Calculate good parts: total produced minus accumulated scrap
const scrapTotal = Number(activeOrder.scrap) || 0;
const totalProduced = cycles * cavities;
const produced = totalProduced - scrapTotal;
const target = Number(activeOrder.target) || 0;
const progress = target > 0 ? Math.min(100, Math.round((produced / target) * 100)) : 0;

activeOrder.good = produced;
activeOrder.progressPercent = progress;
activeOrder.lastUpdateIso = new Date().toISOString();
global.set("activeWorkOrder", activeOrder);

// ============================================================================
// SECTION 6: SCRAP PROMPT (Target Reached)
// ============================================================================
const promptIssued = global.get("scrapPromptIssuedFor") || null;

if (!promptIssued && target > 0 && produced >= target) {
    node.warn(`[DEBUG] TRIGGERING PROMPT - Target reached!`);
    global.set("scrapPromptIssuedFor", activeOrder.id);
    msg._mode = "scrap-prompt";
    msg.scrapPrompt = {
        id: activeOrder.id,
        sku: activeOrder.sku || "",
        target,
        produced
    };
    return [null, msg, null, anomalyMsg]; // bypass the DB update on this cycle
}

// ============================================================================
// SECTION 7: DATABASE UPDATE MESSAGE
// ============================================================================
const dbMsg = {
    _mode: "cycle",
    cycle: {
        id: activeOrder.id,
        sku: activeOrder.sku || "",
        target,
        good: produced,
        scrap: Number(activeOrder.scrap) || 0,
        cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0),
        progressPercent: progress,
        lastUpdateIso: activeOrder.lastUpdateIso,
        machineOnline: true,
        productionStarted: productionRunning
    },
    topic: `
      UPDATE work_orders
      SET
        good_parts = ${produced},
        progress_percent = ${progress},
        updated_at = NOW()
      WHERE work_order_id = '${activeOrder.id}';
    `
};

// ============================================================================
// SECTION 8: STATE BACKUP TO DATABASE (Every 10th cycle - Issue 1)
// ============================================================================
let cyclesSinceBackup = flow.get("cyclesSinceBackup") || 0;
cyclesSinceBackup++;
flow.set("cyclesSinceBackup", cyclesSinceBackup);

let stateBackupMsg = null;
if (cyclesSinceBackup >= 10) {
    // Reset counter
    flow.set("cyclesSinceBackup", 0);

    // Backup current state to database
    const productionStartTime = global.get("productionStartTime") || null;
    const operatingTime = global.get("operatingTime") || 0;
    const downtime = global.get("downtime") || 0;
    const lastUpdateTime = global.get("lastUpdateTime") || null;
    const scrapPromptIssuedFor = global.get("scrapPromptIssuedFor") || null;
    const currentSessionId = global.get("currentSessionId") || null;

    stateBackupMsg = {
        _mode: "state-backup",
        topic: `
          UPDATE session_state
          SET
            work_order_id = '${activeOrder.id}',
            cycle_count = ${cycles},
            production_start_time = ${productionStartTime},
            operating_time = ${operatingTime.toFixed(2)},
            downtime = ${downtime.toFixed(2)},
            last_update_time = ${lastUpdateTime},
            tracking_enabled = ${trackingEnabled ? 1 : 0},
            machine_state = ${current},
            scrap_prompt_issued_for = ${scrapPromptIssuedFor ? `'${scrapPromptIssuedFor}'` : 'NULL'},
            current_session_id = ${currentSessionId ? `'${currentSessionId}'` : 'NULL'}
          WHERE session_key = 'current_session';
        `
    };

    node.warn(`[STATE BACKUP] Saved state to database - Cycle ${cycles}`);
}

// ============================================================================
// OUTPUTS:
// Output 1: Database update for work_orders table
// Output 2: State message to UI / Scrap prompt / Cycle cap alert
// Output 3: State backup to session_state table (every 10 cycles)
// Output 4: Anomaly detection to cycle_anomalies table
// ============================================================================
return [dbMsg, stateMsg, stateBackupMsg, anomalyMsg];


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Review current Node-RED implementation structure

Reviewing current Node-RED implementation structure

Check existing database schema and tables

Checking existing database schema and tables

Analyze current flow logic for KPI tracking

Analyzing current flow logic for KPI tracking

Identify gaps vs optimization requirements

Identifying gaps vs optimization requirements

Create complete database migration script

Creating complete database migration script

Update Machine cycles function with time tracking

Updating Machine cycles function with time tracking

Add third output wire to Machine cycles node

Adding third output wire to Machine cycles node

Create database handler for state backup

Creating database handler for state backup

Update Work Order buttons with stop categorization

Updating Work Order buttons with stop categorization

Create UI for stop reason prompt

Implement session management logic

Add cycle count capping logic

Implement cycle anomaly tracking

Create startup recovery flow

Create enhanced Machine cycles function

Creating enhanced Machine cycles function

Create enhanced Work Order buttons function

Creating enhanced Work Order buttons function

Create database state backup handler

Create stop reason UI prompt

Create startup recovery flow

Update flows.json with all changes

Create implementation guide document


File created successfully at: /home/mdares/.node-red/enhanced_machine_cycles_function.js

// ============================================================================
// ENHANCED "Machine cycles" Function - Complete Implementation
// Location: flows.json, node ID: 0d023d87a13bf56f
// Outputs: 4 (cycle update, state change, state backup, anomaly detection)
//
// Features Implemented:
// - Time tracking (operating time and downtime)
// - State backup to database (every 10 cycles)
// - Cycle count capping at 100
// - Cycle anomaly detection (Issue 3)
// - Session tracking integration
// ============================================================================

const current = Number(msg.payload) || 0;

// ============================================================================
// SECTION 1: TIME TRACKING (Must run BEFORE any early returns)
// ============================================================================
const now = Date.now();
const trackingEnabled = !!global.get("trackingEnabled");
const lastUpdate = global.get("lastUpdateTime") || now;
const deltaMs = now - lastUpdate;
const deltaSeconds = deltaMs / 1000;

// Track last cycle time for anomaly detection
const lastCycleTimestamp = global.get("lastCycleTimestamp") || now;
const cycleTimeMs = now - lastCycleTimestamp;
const cycleTimeSeconds = cycleTimeMs / 1000;

// Sanity check: Protect against clock skew (negative delta or >5 min gap)
if (deltaSeconds < 0 || deltaSeconds > 300) {
    node.warn(`[TIME] Abnormal delta: ${deltaSeconds.toFixed(2)}s - clock skew or restart detected, resetting timer`);
    global.set("lastUpdateTime", now);
    // Don't accumulate time, but continue processing
} else {
    // Normal delta, accumulate time if tracking is enabled
    if (trackingEnabled) {
        // Initialize timing vars if they don't exist (handles restart scenario)
        if (!global.get("productionStartTime")) {
            node.warn("[TIME] Production start time missing, initializing now");
            global.set("productionStartTime", now);
            global.set("operatingTime", 0);
            global.set("downtime", 0);
        }

        // Accumulate operating time when machine is running (state 1)
        if (current === 1) {
            const opTime = global.get("operatingTime") || 0;
            global.set("operatingTime", opTime + deltaSeconds);
        }
        // Accumulate downtime when machine is stopped (state 0) while tracking
        else if (current === 0) {
            const downTime = global.get("downtime") || 0;
            global.set("downtime", downTime + deltaSeconds);
        }
    }
    global.set("lastUpdateTime", now);
}

// ============================================================================
// SECTION 2: EXISTING CYCLE COUNTING LOGIC
// ============================================================================
let zeroStreak = flow.get("zeroStreak") || 0;
zeroStreak = current === 0 ? zeroStreak + 1 : 0;
flow.set("zeroStreak", zeroStreak);

const prev = flow.get("lastMachineState") ?? 0;
flow.set("lastMachineState", current);

global.set("machineOnline", true);   // force ONLINE for now

let productionRunning = !!global.get("productionStarted");
let stateChanged = false;

if (current === 1 && !productionRunning) {
    productionRunning = true;
    stateChanged = true;
} else if (current === 0 && zeroStreak >= 2 && productionRunning) {
    productionRunning = false;
    stateChanged = true;
}

global.set("productionStarted", productionRunning);

const stateMsg = stateChanged
    ? {
        _mode: "production-state",
        machineOnline: true,
        productionStarted: productionRunning
    }
    : null;

const activeOrder = global.get("activeWorkOrder");
const cavities = Number(global.get("moldActive") || 0);
if (!activeOrder || !activeOrder.id || cavities <= 0) {
    // We still want to pass along any state change even if there's no active WO.
    return [null, stateMsg, null, null];
}

// Check if tracking is enabled (START button clicked)
if (!trackingEnabled) {
    // Cycles are happening but we're not tracking them yet
    return [null, stateMsg, null, null];
}

// only count rising edges (0 -> 1) for production totals
if (prev === 1 || current !== 1) {
    return [null, stateMsg, null, null];
}

// ============================================================================
// SECTION 3: CYCLE COUNT WITH CAPPING (Issue 2)
// ============================================================================
let cycles = Number(global.get("cycleCount") || 0);

// Check if we've reached the 100 cycle cap
if (cycles >= 100) {
    node.warn("[CYCLE CAP] Maximum 100 cycles reached. Prompting for work order completion.");

    // Create alert message
    msg._mode = "cycle-cap-reached";
    msg.alert = {
        id: activeOrder.id,
        sku: activeOrder.sku || "",
        cycles: cycles,
        message: "Maximum 100 cycles reached. Please complete the work order or enter scrap parts."
    };

    return [null, msg, null, null];
}

cycles = cycles + 1;
global.set("cycleCount", cycles);
global.set("lastCycleTimestamp", now);

// Show warning when approaching cap
if (cycles >= 90 && cycles < 100) {
    node.warn(`[CYCLE WARNING] Approaching cycle cap: ${cycles}/100 cycles completed`);
}

// ============================================================================
// SECTION 4: CYCLE ANOMALY DETECTION (Issue 3)
// ============================================================================
let anomalyMsg = null;

// Get theoretical cycle time from work order
const theoreticalCycleTime = Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0);

if (theoreticalCycleTime > 0 && cycles > 1) { // Skip first cycle (no baseline)
    // Calculate rolling average (last 10 cycles)
    let cycleTimeHistory = flow.get("cycleTimeHistory") || [];
    cycleTimeHistory.push(cycleTimeSeconds);

    // Keep only last 10 cycles
    if (cycleTimeHistory.length > 10) {
        cycleTimeHistory = cycleTimeHistory.slice(-10);
    }
    flow.set("cycleTimeHistory", cycleTimeHistory);

    // Calculate average
    const avgCycleTime = cycleTimeHistory.reduce((a, b) => a + b, 0) / cycleTimeHistory.length;

    // Calculate deviation from theoretical time
    const deviation = ((cycleTimeSeconds - theoreticalCycleTime) / theoreticalCycleTime) * 100;

    // Flag if deviation is > 20%
    if (Math.abs(deviation) > 20) {
        const anomalyType = deviation > 0 ? "slower" : "faster";

        node.warn(`[ANOMALY] Cycle ${cycles}: ${cycleTimeSeconds.toFixed(2)}s (expected: ${theoreticalCycleTime}s, deviation: ${deviation.toFixed(1)}%)`);

        const sessionId = global.get("currentSessionId") || null;

        anomalyMsg = {
            _mode: "cycle-anomaly",
            topic: `
                INSERT INTO cycle_anomalies
                (work_order_id, session_id, cycle_number, expected_time, actual_time, deviation_percent, anomaly_type, timestamp, notes)
                VALUES
                ('${activeOrder.id}', ${sessionId ? `'${sessionId}'` : 'NULL'}, ${cycles}, ${theoreticalCycleTime}, ${cycleTimeSeconds.toFixed(2)}, ${deviation.toFixed(2)}, '${anomalyType}', ${now}, 'Automatic detection: ${deviation.toFixed(1)}% deviation from expected');
            `
        };
    }
}

// ============================================================================
// SECTION 5: GOOD PARTS CALCULATION
// ============================================================================
// Calculate good parts: total produced minus accumulated scrap
const scrapTotal = Number(activeOrder.scrap) || 0;
const totalProduced = cycles * cavities;
const produced = totalProduced - scrapTotal;
const target = Number(activeOrder.target) || 0;
const progress = target > 0 ? Math.min(100, Math.round((produced / target) * 100)) : 0;

activeOrder.good = produced;
activeOrder.progressPercent = progress;
activeOrder.lastUpdateIso = new Date().toISOString();
global.set("activeWorkOrder", activeOrder);

// ============================================================================
// SECTION 6: SCRAP PROMPT (Target Reached)
// ============================================================================
const promptIssued = global.get("scrapPromptIssuedFor") || null;

if (!promptIssued && target > 0 && produced >= target) {
    node.warn(`[DEBUG] TRIGGERING PROMPT - Target reached!`);
    global.set("scrapPromptIssuedFor", activeOrder.id);
    msg._mode = "scrap-prompt";
    msg.scrapPrompt = {
        id: activeOrder.id,
        sku: activeOrder.sku || "",
        target,
        produced
    };
    return [null, msg, null, anomalyMsg]; // bypass the DB update on this cycle
}

// ============================================================================
// SECTION 7: DATABASE UPDATE MESSAGE
// ============================================================================
const dbMsg = {
    _mode: "cycle",
    cycle: {
        id: activeOrder.id,
        sku: activeOrder.sku || "",
        target,
        good: produced,
        scrap: Number(activeOrder.scrap) || 0,
        cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0),
        progressPercent: progress,
        lastUpdateIso: activeOrder.lastUpdateIso,
        machineOnline: true,
        productionStarted: productionRunning
    },
    topic: `
      UPDATE work_orders
      SET
        good_parts = ${produced},
        progress_percent = ${progress},
        updated_at = NOW()
      WHERE work_order_id = '${activeOrder.id}';
    `
};

// ============================================================================
// SECTION 8: STATE BACKUP TO DATABASE (Every 10th cycle - Issue 1)
// ============================================================================
let cyclesSinceBackup = flow.get("cyclesSinceBackup") || 0;
cyclesSinceBackup++;
flow.set("cyclesSinceBackup", cyclesSinceBackup);

let stateBackupMsg = null;
if (cyclesSinceBackup >= 10) {
    // Reset counter
    flow.set("cyclesSinceBackup", 0);

    // Backup current state to database
    const productionStartTime = global.get("productionStartTime") || null;
    const operatingTime = global.get("operatingTime") || 0;
    const downtime = global.get("downtime") || 0;
    const lastUpdateTime = global.get("lastUpdateTime") || null;
    const scrapPromptIssuedFor = global.get("scrapPromptIssuedFor") || null;
    const currentSessionId = global.get("currentSessionId") || null;

    stateBackupMsg = {
        _mode: "state-backup",
        topic: `
          UPDATE session_state
          SET
            work_order_id = '${activeOrder.id}',
            cycle_count = ${cycles},
            production_start_time = ${productionStartTime},
            operating_time = ${operatingTime.toFixed(2)},
            downtime = ${downtime.toFixed(2)},
            last_update_time = ${lastUpdateTime},
            tracking_enabled = ${trackingEnabled ? 1 : 0},
            machine_state = ${current},
            scrap_prompt_issued_for = ${scrapPromptIssuedFor ? `'${scrapPromptIssuedFor}'` : 'NULL'},
            current_session_id = ${currentSessionId ? `'${currentSessionId}'` : 'NULL'}
          WHERE session_key = 'current_session';
        `
    };

    node.warn(`[STATE BACKUP] Saved state to database - Cycle ${cycles}`);
}

// ============================================================================
// OUTPUTS:
// Output 1: Database update for work_orders table
// Output 2: State message to UI / Scrap prompt / Cycle cap alert
// Output 3: State backup to session_state table (every 10 cycles)
// Output 4: Anomaly detection to cycle_anomalies table
// ============================================================================
return [dbMsg, stateMsg, stateBackupMsg, anomalyMsg];


Great! Now I need to create the enhanced Work Order buttons function that includes:1. Stop reason prompt (Issue 4)
2. Session management on START/RESUME (Issue 5)
3. Tracking of stop events4. Session creation and management


TodoWrite

Review current Node-RED implementation structure

Reviewing current Node-RED implementation structure

Check existing database schema and tables

Checking existing database schema and tables

Analyze current flow logic for KPI tracking

Analyzing current flow logic for KPI tracking

Identify gaps vs optimization requirements

Identifying gaps vs optimization requirements

Create complete database migration script

Creating complete database migration script

Create enhanced Machine cycles function

Creating enhanced Machine cycles function

Create enhanced Work Order buttons function

Creating enhanced Work Order buttons function

Create database state backup handler

Create stop reason UI prompt

Create startup recovery flow

Update flows.json with all changes

Create implementation guide document


Write

// ============================================================================
// ENHANCED "Work Order buttons" Function - Complete Implementation
// Location: flows.json, node ID: 9bbd4fade968036d
// Outputs: 5 (upload, select, start/stop, complete, session management)
//
// Features Implemented:
// - Stop reason prompt (Issue 4)
// - Session management (Issue 5)
// - Stop event tracking with categorization
// - Session creation on START/RESUME
// - Automatic downtime calculation based on stop reason
// ============================================================================

// Helper function to generate session ID
function generateSessionId() {
    return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}

switch (msg.action) {
    case "upload-excel":
        msg._mode = "upload";
        return [msg, null, null, null, null];

    case "refresh-work-orders":
        msg._mode = "select";
        msg.topic = "SELECT * FROM work_orders ORDER BY created_at DESC;";
        return [null, msg, null, null, null];

    // ========================================================================
    // START WORK ORDER - Creates new session
    // ========================================================================
    case "start-work-order": {
        msg._mode = "start";
        const order = msg.payload || {};
        if (!order.id) {
            node.error("No work order id supplied for start", msg);
            return [null, null, null, null, null];
        }

        msg.startOrder = order;

        // Update work order status
        msg.topic = `
            UPDATE work_orders
            SET
                status = CASE
                    WHEN work_order_id = '${order.id}' THEN 'RUNNING'
                    ELSE 'PENDING'
                END,
                updated_at = CASE
                    WHEN work_order_id = '${order.id}' THEN NOW()
                    ELSE updated_at
                END
            WHERE status <> 'DONE';
        `;

        // Set up global state
        global.set("activeWorkOrder", order);
        global.set("cycleCount", 0);
        flow.set("lastMachineState", 0);
        global.set("scrapPromptIssuedFor", null);

        // Create new session (Issue 5)
        const sessionId = generateSessionId();
        global.set("currentSessionId", sessionId);
        global.set("productionStartTime", Date.now());
        global.set("operatingTime", 0);
        global.set("downtime", 0);
        global.set("lastUpdateTime", Date.now());

        // Create session record
        const sessionMsg = {
            _mode: "create-session",
            topic: `
                INSERT INTO production_sessions
                (session_id, work_order_id, start_time, reason_for_start, cycles_completed, operating_time, downtime)
                VALUES
                ('${sessionId}', '${order.id}', ${Date.now()}, 'initial_start', 0, 0, 0);
            `
        };

        node.warn(`[SESSION] Created new session: ${sessionId}`);

        return [null, null, msg, null, sessionMsg];
    }

    // ========================================================================
    // COMPLETE WORK ORDER - Ends session
    // ========================================================================
    case "complete-work-order": {
        msg._mode = "complete";
        const order = msg.payload || {};
        if (!order.id) {
            node.error("No work order id supplied for complete", msg);
            return [null, null, null, null, null];
        }

        msg.completeOrder = order;

        // Update work order status
        msg.topic = `
            UPDATE work_orders
            SET status = 'DONE', updated_at = NOW()
            WHERE work_order_id = '${order.id}';
        `;

        // Get session data to close it
        const sessionId = global.get("currentSessionId");
        const cycles = global.get("cycleCount") || 0;
        const operatingTime = global.get("operatingTime") || 0;
        const downtime = global.get("downtime") || 0;
        const productionStartTime = global.get("productionStartTime") || Date.now();
        const now = Date.now();
        const duration = (now - productionStartTime) / 1000;

        let sessionMsg = null;
        if (sessionId) {
            // Close the current session
            sessionMsg = {
                _mode: "close-session",
                topic: `
                    UPDATE production_sessions
                    SET
                        end_time = ${now},
                        duration = ${duration.toFixed(2)},
                        cycles_completed = ${cycles},
                        operating_time = ${operatingTime.toFixed(2)},
                        downtime = ${downtime.toFixed(2)},
                        reason_for_end = 'work_order_complete'
                    WHERE session_id = '${sessionId}';
                `
            };

            // Update work order totals
            msg.topic += `
                UPDATE work_orders
                SET
                    total_sessions = (SELECT COUNT(*) FROM production_sessions WHERE work_order_id = '${order.id}'),
                    total_operating_time = (SELECT SUM(operating_time) FROM production_sessions WHERE work_order_id = '${order.id}'),
                    total_downtime = (SELECT SUM(downtime) FROM production_sessions WHERE work_order_id = '${order.id}'),
                    avg_session_duration = (SELECT AVG(duration) FROM production_sessions WHERE work_order_id = '${order.id}')
                WHERE work_order_id = '${order.id}';
            `;

            node.warn(`[SESSION] Closed session: ${sessionId}`);
        }

        // Clear global state
        global.set("activeWorkOrder", null);
        global.set("cycleCount", 0);
        flow.set("lastMachineState", 0);
        global.set("scrapPromptIssuedFor", null);
        global.set("currentSessionId", null);
        global.set("trackingEnabled", false);
        global.set("productionStartTime", null);
        global.set("operatingTime", 0);
        global.set("downtime", 0);

        return [null, null, null, msg, sessionMsg];
    }

    // ========================================================================
    // SCRAP ENTRY
    // ========================================================================
    case "scrap-entry": {
        const { id, scrap } = msg.payload || {};
        const scrapNum = Number(scrap) || 0;

        if (!id) {
            node.error("No work order id supplied for scrap entry", msg);
            return [null, null, null, null, null];
        }

        // Update activeWorkOrder with accumulated scrap
        const activeOrder = global.get("activeWorkOrder");
        if (activeOrder && activeOrder.id === id) {
            activeOrder.scrap = (Number(activeOrder.scrap) || 0) + scrapNum;
            global.set("activeWorkOrder", activeOrder);
        }

        // Clear prompt flag so it can show again when target reached next time
        global.set("scrapPromptIssuedFor", null);

        msg._mode = "scrap-update";
        msg.scrapEntry = { id, scrap: scrapNum };
        msg.topic = `
            UPDATE work_orders
            SET
              scrap_count = scrap_count + ${scrapNum},
              updated_at = NOW()
            WHERE work_order_id = '${id}';
        `;

        return [null, null, msg, null, null];
    }

    // ========================================================================
    // SCRAP SKIP
    // ========================================================================
    case "scrap-skip": {
        const { id } = msg.payload || {};

        if (!id) {
            node.error("No work order id supplied for scrap skip", msg);
            return [null, null, null, null, null];
        }

        msg._mode = "scrap-skipped";
        return [null, null, null, null, null];
    }

    // ========================================================================
    // START (RESUME) - Creates new session (Issue 5)
    // ========================================================================
    case "start": {
        // START/RESUME button clicked from Home dashboard
        // Enable tracking and create new session

        const now = Date.now();
        const wasTracking = !!global.get("trackingEnabled");
        const activeOrder = global.get("activeWorkOrder");

        // Close previous session if exists
        let closeSessionMsg = null;
        const prevSessionId = global.get("currentSessionId");

        if (prevSessionId && wasTracking === false) {
            // There was a stop, close the previous session
            const prevStartTime = global.get("productionStartTime") || now;
            const sessionDuration = (now - prevStartTime) / 1000;
            const cycles = global.get("cycleCount") || 0;
            const operatingTime = global.get("operatingTime") || 0;
            const downtime = global.get("downtime") || 0;

            // Get the last stop event to determine reason
            const lastStopReason = flow.get("lastStopReason") || "unknown";
            const lastStopCategory = flow.get("lastStopCategory") || "unplanned";

            closeSessionMsg = {
                _mode: "close-session",
                topic: `
                    UPDATE production_sessions
                    SET
                        end_time = ${now},
                        duration = ${sessionDuration.toFixed(2)},
                        cycles_completed = ${cycles},
                        operating_time = ${operatingTime.toFixed(2)},
                        downtime = ${downtime.toFixed(2)},
                        reason_for_end = '${lastStopCategory === 'planned' ? 'planned_stop' : 'unplanned_stop'}'
                    WHERE session_id = '${prevSessionId}';
                `
            };
        }

        // Enable tracking
        global.set("trackingEnabled", true);

        // Create new session
        const newSessionId = generateSessionId();
        global.set("currentSessionId", newSessionId);
        global.set("productionStartTime", now);
        global.set("lastUpdateTime", now);

        const reasonForStart = wasTracking ? "resume_after_unplanned" :
            (flow.get("lastStopCategory") === "planned" ? "resume_after_planned" : "resume_after_unplanned");

        const newSessionMsg = {
            _mode: "create-session",
            topic: `
                INSERT INTO production_sessions
                (session_id, work_order_id, start_time, reason_for_start, cycles_completed, operating_time, downtime)
                VALUES
                ('${newSessionId}', ${activeOrder ? `'${activeOrder.id}'` : 'NULL'}, ${now}, '${reasonForStart}', 0, 0, 0);
            `
        };

        // Also resume any open stop event
        const lastStopEventId = flow.get("lastStopEventId");
        let resumeStopMsg = null;

        if (lastStopEventId) {
            const stopTime = flow.get("lastStopTime") || now;
            const stopDuration = (now - stopTime) / 1000;

            resumeStopMsg = {
                _mode: "resume-stop",
                topic: `
                    UPDATE stop_events
                    SET
                        resume_time = ${now},
                        duration = ${stopDuration.toFixed(2)}
                    WHERE id = ${lastStopEventId};
                `
            };

            flow.set("lastStopEventId", null);
        }

        node.warn(`[SESSION] Started new session: ${newSessionId} (reason: ${reasonForStart})`);

        // Combine all session messages into output 5
        const combinedSessionMsg = closeSessionMsg || newSessionMsg;
        if (closeSessionMsg && newSessionMsg) {
            // Need to send both - combine topics
            combinedSessionMsg.topic = closeSessionMsg.topic + "\n" + newSessionMsg.topic;
            if (resumeStopMsg) {
                combinedSessionMsg.topic += "\n" + resumeStopMsg.topic;
            }
        } else if (resumeStopMsg) {
            combinedSessionMsg.topic += "\n" + resumeStopMsg.topic;
        }

        return [null, null, null, null, combinedSessionMsg];
    }

    // ========================================================================
    // STOP - Shows prompt for stop reason (Issue 4)
    // ========================================================================
    case "stop": {
        // STOP button clicked - show prompt for categorization
        msg._mode = "stop-prompt";
        msg.stopPrompt = {
            timestamp: Date.now(),
            workOrderId: (global.get("activeWorkOrder") || {}).id || null
        };

        node.warn("[STOP] Showing stop reason prompt");

        return [null, msg, null, null, null];
    }

    // ========================================================================
    // STOP REASON SUBMITTED (Issue 4)
    // ========================================================================
    case "stop-reason": {
        const { category, reason, notes } = msg.payload || {};

        if (!category || !reason) {
            node.error("Stop reason category and detail required", msg);
            return [null, null, null, null, null];
        }

        const now = Date.now();
        const activeOrder = global.get("activeWorkOrder");
        const sessionId = global.get("currentSessionId");

        // Determine if this affects availability
        const affectsAvailability = (category === 'unplanned') ? 1 : 0;

        // Create stop event
        const stopEventMsg = {
            _mode: "create-stop-event",
            topic: `
                INSERT INTO stop_events
                (work_order_id, session_id, stop_time, reason_category, reason_detail, affects_availability, operator_notes)
                VALUES
                (${activeOrder ? `'${activeOrder.id}'` : 'NULL'}, ${sessionId ? `'${sessionId}'` : 'NULL'}, ${now}, '${category}', '${reason}', ${affectsAvailability}, ${notes ? `'${notes.replace(/'/g, "''")}'` : 'NULL'});

                SELECT LAST_INSERT_ID() as stop_event_id;
            `
        };

        // Store for later use when RESUME is clicked
        flow.set("lastStopReason", reason);
        flow.set("lastStopCategory", category);
        flow.set("lastStopTime", now);
        // Note: We'll get the stop_event_id from the query result and store it

        // Disable tracking
        global.set("trackingEnabled", false);

        node.warn(`[STOP] Recorded ${category} stop: ${reason}`);

        return [null, null, null, null, stopEventMsg];
    }

    // ========================================================================
    // STORE STOP EVENT ID (called after INSERT)
    // ========================================================================
    case "store-stop-event-id": {
        const stopEventId = msg.payload;
        if (stopEventId) {
            flow.set("lastStopEventId", stopEventId);
            node.warn(`[STOP] Stored stop event ID: ${stopEventId}`);
        }
        return [null, null, null, null, null];
    }
}

// Default - no action matched
return [null, null, null, null, null];


Create enhanced Machine cycles function

Create database state backup handler

Create stop reason UI prompt

Create startup recovery flow

Update flows.json with all changes

Create implementation guide document

Create enhanced Machine cycles function

Create database state backup handler

Create stop reason UI prompt

Create startup recovery flow

Update flows.json with all changes

Create implementation guide document


File created successfully at: /home/mdares/.node-red/enhanced_work_order_buttons_function.js

// ============================================================================
// ENHANCED "Work Order buttons" Function - Complete Implementation
// Location: flows.json, node ID: 9bbd4fade968036d
// Outputs: 5 (upload, select, start/stop, complete, session management)
//
// Features Implemented:
// - Stop reason prompt (Issue 4)
// - Session management (Issue 5)
// - Stop event tracking with categorization
// - Session creation on START/RESUME
// - Automatic downtime calculation based on stop reason
// ============================================================================

// Helper function to generate session ID
function generateSessionId() {
    return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}

switch (msg.action) {
    case "upload-excel":
        msg._mode = "upload";
        return [msg, null, null, null, null];

    case "refresh-work-orders":
        msg._mode = "select";
        msg.topic = "SELECT * FROM work_orders ORDER BY created_at DESC;";
        return [null, msg, null, null, null];

    // ========================================================================
    // START WORK ORDER - Creates new session
    // ========================================================================
    case "start-work-order": {
        msg._mode = "start";
        const order = msg.payload || {};
        if (!order.id) {
            node.error("No work order id supplied for start", msg);
            return [null, null, null, null, null];
        }

        msg.startOrder = order;

        // Update work order status
        msg.topic = `
            UPDATE work_orders
            SET
                status = CASE
                    WHEN work_order_id = '${order.id}' THEN 'RUNNING'
                    ELSE 'PENDING'
                END,
                updated_at = CASE
                    WHEN work_order_id = '${order.id}' THEN NOW()
                    ELSE updated_at
                END
            WHERE status <> 'DONE';
        `;

        // Set up global state
        global.set("activeWorkOrder", order);
        global.set("cycleCount", 0);
        flow.set("lastMachineState", 0);
        global.set("scrapPromptIssuedFor", null);

        // Create new session (Issue 5)
        const sessionId = generateSessionId();
        global.set("currentSessionId", sessionId);
        global.set("productionStartTime", Date.now());
        global.set("operatingTime", 0);
        global.set("downtime", 0);
        global.set("lastUpdateTime", Date.now());

        // Create session record
        const sessionMsg = {
            _mode: "create-session",
            topic: `
                INSERT INTO production_sessions
                (session_id, work_order_id, start_time, reason_for_start, cycles_completed, operating_time, downtime)
                VALUES
                ('${sessionId}', '${order.id}', ${Date.now()}, 'initial_start', 0, 0, 0);
            `
        };

        node.warn(`[SESSION] Created new session: ${sessionId}`);

        return [null, null, msg, null, sessionMsg];
    }

    // ========================================================================
    // COMPLETE WORK ORDER - Ends session
    // ========================================================================
    case "complete-work-order": {
        msg._mode = "complete";
        const order = msg.payload || {};
        if (!order.id) {
            node.error("No work order id supplied for complete", msg);
            return [null, null, null, null, null];
        }

        msg.completeOrder = order;

        // Update work order status
        msg.topic = `
            UPDATE work_orders
            SET status = 'DONE', updated_at = NOW()
            WHERE work_order_id = '${order.id}';
        `;

        // Get session data to close it
        const sessionId = global.get("currentSessionId");
        const cycles = global.get("cycleCount") || 0;
        const operatingTime = global.get("operatingTime") || 0;
        const downtime = global.get("downtime") || 0;
        const productionStartTime = global.get("productionStartTime") || Date.now();
        const now = Date.now();
        const duration = (now - productionStartTime) / 1000;

        let sessionMsg = null;
        if (sessionId) {
            // Close the current session
            sessionMsg = {
                _mode: "close-session",
                topic: `
                    UPDATE production_sessions
                    SET
                        end_time = ${now},
                        duration = ${duration.toFixed(2)},
                        cycles_completed = ${cycles},
                        operating_time = ${operatingTime.toFixed(2)},
                        downtime = ${downtime.toFixed(2)},
                        reason_for_end = 'work_order_complete'
                    WHERE session_id = '${sessionId}';
                `
            };

            // Update work order totals
            msg.topic += `
                UPDATE work_orders
                SET
                    total_sessions = (SELECT COUNT(*) FROM production_sessions WHERE work_order_id = '${order.id}'),
                    total_operating_time = (SELECT SUM(operating_time) FROM production_sessions WHERE work_order_id = '${order.id}'),
                    total_downtime = (SELECT SUM(downtime) FROM production_sessions WHERE work_order_id = '${order.id}'),
                    avg_session_duration = (SELECT AVG(duration) FROM production_sessions WHERE work_order_id = '${order.id}')
                WHERE work_order_id = '${order.id}';
            `;

            node.warn(`[SESSION] Closed session: ${sessionId}`);
        }

        // Clear global state
        global.set("activeWorkOrder", null);
        global.set("cycleCount", 0);
        flow.set("lastMachineState", 0);
        global.set("scrapPromptIssuedFor", null);
        global.set("currentSessionId", null);
        global.set("trackingEnabled", false);
        global.set("productionStartTime", null);
        global.set("operatingTime", 0);
        global.set("downtime", 0);

        return [null, null, null, msg, sessionMsg];
    }

    // ========================================================================
    // SCRAP ENTRY
    // ========================================================================
    case "scrap-entry": {
        const { id, scrap } = msg.payload || {};
        const scrapNum = Number(scrap) || 0;

        if (!id) {
            node.error("No work order id supplied for scrap entry", msg);
            return [null, null, null, null, null];
        }

        // Update activeWorkOrder with accumulated scrap
        const activeOrder = global.get("activeWorkOrder");
        if (activeOrder && activeOrder.id === id) {
            activeOrder.scrap = (Number(activeOrder.scrap) || 0) + scrapNum;
            global.set("activeWorkOrder", activeOrder);
        }

        // Clear prompt flag so it can show again when target reached next time
        global.set("scrapPromptIssuedFor", null);

        msg._mode = "scrap-update";
        msg.scrapEntry = { id, scrap: scrapNum };
        msg.topic = `
            UPDATE work_orders
            SET
              scrap_count = scrap_count + ${scrapNum},
              updated_at = NOW()
            WHERE work_order_id = '${id}';
        `;

        return [null, null, msg, null, null];
    }

    // ========================================================================
    // SCRAP SKIP
    // ========================================================================
    case "scrap-skip": {
        const { id } = msg.payload || {};

        if (!id) {
            node.error("No work order id supplied for scrap skip", msg);
            return [null, null, null, null, null];
        }

        msg._mode = "scrap-skipped";
        return [null, null, null, null, null];
    }

    // ========================================================================
    // START (RESUME) - Creates new session (Issue 5)
    // ========================================================================
    case "start": {
        // START/RESUME button clicked from Home dashboard
        // Enable tracking and create new session

        const now = Date.now();
        const wasTracking = !!global.get("trackingEnabled");
        const activeOrder = global.get("activeWorkOrder");

        // Close previous session if exists
        let closeSessionMsg = null;
        const prevSessionId = global.get("currentSessionId");

        if (prevSessionId && wasTracking === false) {
            // There was a stop, close the previous session
            const prevStartTime = global.get("productionStartTime") || now;
            const sessionDuration = (now - prevStartTime) / 1000;
            const cycles = global.get("cycleCount") || 0;
            const operatingTime = global.get("operatingTime") || 0;
            const downtime = global.get("downtime") || 0;

            // Get the last stop event to determine reason
            const lastStopReason = flow.get("lastStopReason") || "unknown";
            const lastStopCategory = flow.get("lastStopCategory") || "unplanned";

            closeSessionMsg = {
                _mode: "close-session",
                topic: `
                    UPDATE production_sessions
                    SET
                        end_time = ${now},
                        duration = ${sessionDuration.toFixed(2)},
                        cycles_completed = ${cycles},
                        operating_time = ${operatingTime.toFixed(2)},
                        downtime = ${downtime.toFixed(2)},
                        reason_for_end = '${lastStopCategory === 'planned' ? 'planned_stop' : 'unplanned_stop'}'
                    WHERE session_id = '${prevSessionId}';
                `
            };
        }

        // Enable tracking
        global.set("trackingEnabled", true);

        // Create new session
        const newSessionId = generateSessionId();
        global.set("currentSessionId", newSessionId);
        global.set("productionStartTime", now);
        global.set("lastUpdateTime", now);

        const reasonForStart = wasTracking ? "resume_after_unplanned" :
            (flow.get("lastStopCategory") === "planned" ? "resume_after_planned" : "resume_after_unplanned");

        const newSessionMsg = {
            _mode: "create-session",
            topic: `
                INSERT INTO production_sessions
                (session_id, work_order_id, start_time, reason_for_start, cycles_completed, operating_time, downtime)
                VALUES
                ('${newSessionId}', ${activeOrder ? `'${activeOrder.id}'` : 'NULL'}, ${now}, '${reasonForStart}', 0, 0, 0);
            `
        };

        // Also resume any open stop event
        const lastStopEventId = flow.get("lastStopEventId");
        let resumeStopMsg = null;

        if (lastStopEventId) {
            const stopTime = flow.get("lastStopTime") || now;
            const stopDuration = (now - stopTime) / 1000;

            resumeStopMsg = {
                _mode: "resume-stop",
                topic: `
                    UPDATE stop_events
                    SET
                        resume_time = ${now},
                        duration = ${stopDuration.toFixed(2)}
                    WHERE id = ${lastStopEventId};
                `
            };

            flow.set("lastStopEventId", null);
        }

        node.warn(`[SESSION] Started new session: ${newSessionId} (reason: ${reasonForStart})`);

        // Combine all session messages into output 5
        const combinedSessionMsg = closeSessionMsg || newSessionMsg;
        if (closeSessionMsg && newSessionMsg) {
            // Need to send both - combine topics
            combinedSessionMsg.topic = closeSessionMsg.topic + "\n" + newSessionMsg.topic;
            if (resumeStopMsg) {
                combinedSessionMsg.topic += "\n" + resumeStopMsg.topic;
            }
        } else if (resumeStopMsg) {
            combinedSessionMsg.topic += "\n" + resumeStopMsg.topic;
        }

        return [null, null, null, null, combinedSessionMsg];
    }

    // ========================================================================
    // STOP - Shows prompt for stop reason (Issue 4)
    // ========================================================================
    case "stop": {
        // STOP button clicked - show prompt for categorization
        msg._mode = "stop-prompt";
        msg.stopPrompt = {
            timestamp: Date.now(),
            workOrderId: (global.get("activeWorkOrder") || {}).id || null
        };

        node.warn("[STOP] Showing stop reason prompt");

        return [null, msg, null, null, null];
    }

    // ========================================================================
    // STOP REASON SUBMITTED (Issue 4)
    // ========================================================================
    case "stop-reason": {
        const { category, reason, notes } = msg.payload || {};

        if (!category || !reason) {
            node.error("Stop reason category and detail required", msg);
            return [null, null, null, null, null];
        }

        const now = Date.now();
        const activeOrder = global.get("activeWorkOrder");
        const sessionId = global.get("currentSessionId");

        // Determine if this affects availability
        const affectsAvailability = (category === 'unplanned') ? 1 : 0;

        // Create stop event
        const stopEventMsg = {
            _mode: "create-stop-event",
            topic: `
                INSERT INTO stop_events
                (work_order_id, session_id, stop_time, reason_category, reason_detail, affects_availability, operator_notes)
                VALUES
                (${activeOrder ? `'${activeOrder.id}'` : 'NULL'}, ${sessionId ? `'${sessionId}'` : 'NULL'}, ${now}, '${category}', '${reason}', ${affectsAvailability}, ${notes ? `'${notes.replace(/'/g, "''")}'` : 'NULL'});

                SELECT LAST_INSERT_ID() as stop_event_id;
            `
        };

        // Store for later use when RESUME is clicked
        flow.set("lastStopReason", reason);
        flow.set("lastStopCategory", category);
        flow.set("lastStopTime", now);
        // Note: We'll get the stop_event_id from the query result and store it

        // Disable tracking
        global.set("trackingEnabled", false);

        node.warn(`[STOP] Recorded ${category} stop: ${reason}`);

        return [null, null, null, null, stopEventMsg];
    }

    // ========================================================================
    // STORE STOP EVENT ID (called after INSERT)
    // ========================================================================
    case "store-stop-event-id": {
        const stopEventId = msg.payload;
        if (stopEventId) {
            flow.set("lastStopEventId", stopEventId);
            node.warn(`[STOP] Stored stop event ID: ${stopEventId}`);
        }
        return [null, null, null, null, null];
    }
}

// Default - no action matched
return [null, null, null, null, null];


Excellent! Now I need to create:1. A startup recovery flow/function
2. Implementation guide documentLet me create the startup recovery function first.


TodoWrite

Review current Node-RED implementation structure

Reviewing current Node-RED implementation structure

Check existing database schema and tables

Checking existing database schema and tables

Analyze current flow logic for KPI tracking

Analyzing current flow logic for KPI tracking

Identify gaps vs optimization requirements

Identifying gaps vs optimization requirements

Create complete database migration script

Creating complete database migration script

Create enhanced Machine cycles function

Creating enhanced Machine cycles function

Create enhanced Work Order buttons function

Creating enhanced Work Order buttons function

Create startup recovery function

Create stop reason UI components

Create implementation guide document


Write

// ============================================================================
// STARTUP RECOVERY Function - Session State Restoration (Issue 1)
// Purpose: Restore session state from database on Node-RED startup/crash recovery
// Trigger: Inject node on startup
// Outputs: 2 (UI notification, database query)
// ============================================================================

// This function should be called on Node-RED startup (via inject node with "inject once after X seconds")
// It queries the session_state table and restores global variables

const mode = msg.mode || "check";

switch (mode) {
    // ========================================================================
    // STEP 1: Query database for session state
    // ========================================================================
    case "check": {
        msg._mode = "query-session-state";
        msg.topic = "SELECT * FROM session_state WHERE session_key = 'current_session';";

        node.warn("[RECOVERY] Checking for existing session state...");

        return [null, msg];
    }

    // ========================================================================
    // STEP 2: Process query results and prompt user
    // ========================================================================
    case "process-results": {
        const results = msg.payload;

        if (!results || results.length === 0) {
            node.warn("[RECOVERY] No session state found in database");
            return [null, null];
        }

        const session = results[0];

        // Check if there's a valid session to restore
        if (!session.work_order_id || session.cycle_count === 0) {
            node.warn("[RECOVERY] Session state exists but is empty - nothing to restore");
            return [null, null];
        }

        // Check if tracking was enabled
        if (!session.tracking_enabled) {
            node.warn("[RECOVERY] Previous session was not actively tracking - restoring state without prompt");

            // Restore the state silently
            global.set("cycleCount", session.cycle_count || 0);
            global.set("productionStartTime", session.production_start_time);
            global.set("operatingTime", Number(session.operating_time) || 0);
            global.set("downtime", Number(session.downtime) || 0);
            global.set("lastUpdateTime", session.last_update_time || Date.now());
            global.set("trackingEnabled", false);
            global.set("scrapPromptIssuedFor", session.scrap_prompt_issued_for || null);
            global.set("currentSessionId", session.current_session_id || null);

            return [null, null];
        }

        // Session was actively tracking - prompt user
        node.warn(`[RECOVERY] Found active session - Work Order: ${session.work_order_id}, Cycles: ${session.cycle_count}`);

        const promptMsg = {
            _mode: "recovery-prompt",
            recoveryPrompt: {
                workOrderId: session.work_order_id,
                cycleCount: session.cycle_count,
                operatingTime: Number(session.operating_time) || 0,
                downtime: Number(session.downtime) || 0,
                timestamp: session.last_update_time,
                sessionData: session
            }
        };

        return [promptMsg, null];
    }

    // ========================================================================
    // STEP 3: User chose to restore session
    // ========================================================================
    case "restore": {
        const sessionData = msg.payload;

        if (!sessionData) {
            node.error("[RECOVERY] No session data provided for restore");
            return [null, null];
        }

        node.warn(`[RECOVERY] Restoring session - Work Order: ${sessionData.work_order_id}, Cycles: ${sessionData.cycle_count}`);

        // Restore all global variables
        global.set("cycleCount", sessionData.cycle_count || 0);
        global.set("productionStartTime", sessionData.production_start_time);
        global.set("operatingTime", Number(sessionData.operating_time) || 0);
        global.set("downtime", Number(sessionData.downtime) || 0);
        global.set("lastUpdateTime", sessionData.last_update_time || Date.now());
        global.set("trackingEnabled", !!sessionData.tracking_enabled);
        global.set("scrapPromptIssuedFor", sessionData.scrap_prompt_issued_for || null);
        global.set("currentSessionId", sessionData.current_session_id || null);

        // Also need to restore activeWorkOrder from work_orders table
        const queryMsg = {
            _mode: "query-work-order",
            topic: `SELECT * FROM work_orders WHERE work_order_id = '${sessionData.work_order_id}';`
        };

        const notificationMsg = {
            _mode: "recovery-success",
            notification: {
                message: `Session restored: ${sessionData.work_order_id} - ${sessionData.cycle_count} cycles`,
                type: "success"
            }
        };

        return [notificationMsg, queryMsg];
    }

    // ========================================================================
    // STEP 4: Set activeWorkOrder from query result
    // ========================================================================
    case "set-work-order": {
        const results = msg.payload;

        if (!results || results.length === 0) {
            node.error("[RECOVERY] Work order not found in database");
            return [null, null];
        }

        const workOrder = results[0];

        // Reconstruct activeWorkOrder object
        const activeOrder = {
            id: workOrder.work_order_id,
            sku: workOrder.sku,
            target: workOrder.target,
            good: workOrder.good_parts,
            scrap: workOrder.scrap_count || 0,
            cycleTime: workOrder.cycle_time,
            theoreticalCycleTime: workOrder.cycle_time,
            progressPercent: workOrder.progress_percent,
            status: workOrder.status,
            lastUpdateIso: new Date().toISOString()
        };

        global.set("activeWorkOrder", activeOrder);

        node.warn(`[RECOVERY] Active work order restored: ${activeOrder.id}`);

        return [null, null];
    }

    // ========================================================================
    // STEP 5: User chose to start fresh
    // ========================================================================
    case "start-fresh": {
        node.warn("[RECOVERY] User chose to start fresh - clearing session state");

        // Clear all global variables
        global.set("cycleCount", 0);
        global.set("productionStartTime", null);
        global.set("operatingTime", 0);
        global.set("downtime", 0);
        global.set("lastUpdateTime", Date.now());
        global.set("trackingEnabled", false);
        global.set("activeWorkOrder", null);
        global.set("scrapPromptIssuedFor", null);
        global.set("currentSessionId", null);

        // Clear session_state in database
        const clearMsg = {
            _mode: "clear-session-state",
            topic: `
                UPDATE session_state
                SET
                    work_order_id = NULL,
                    cycle_count = 0,
                    production_start_time = NULL,
                    operating_time = 0,
                    downtime = 0,
                    last_update_time = NULL,
                    tracking_enabled = 0,
                    machine_state = 0,
                    scrap_prompt_issued_for = NULL,
                    current_session_id = NULL
                WHERE session_key = 'current_session';
            `
        };

        const notificationMsg = {
            _mode: "recovery-cleared",
            notification: {
                message: "Started with fresh session",
                type: "info"
            }
        };

        return [notificationMsg, clearMsg];
    }
}

// Default
return [null, null];


Create enhanced Machine cycles function

Create database state backup handler

Create stop reason UI prompt

Create startup recovery flow

Update flows.json with all changes

Create implementation guide document

Create enhanced Machine cycles function

Create startup recovery function

Create stop reason UI components

Create implementation guide document


File created successfully at: /home/mdares/.node-red/startup_recovery_function.js

// ============================================================================
// STARTUP RECOVERY Function - Session State Restoration (Issue 1)
// Purpose: Restore session state from database on Node-RED startup/crash recovery
// Trigger: Inject node on startup
// Outputs: 2 (UI notification, database query)
// ============================================================================

// This function should be called on Node-RED startup (via inject node with "inject once after X seconds")
// It queries the session_state table and restores global variables

const mode = msg.mode || "check";

switch (mode) {
    // ========================================================================
    // STEP 1: Query database for session state
    // ========================================================================
    case "check": {
        msg._mode = "query-session-state";
        msg.topic = "SELECT * FROM session_state WHERE session_key = 'current_session';";

        node.warn("[RECOVERY] Checking for existing session state...");

        return [null, msg];
    }

    // ========================================================================
    // STEP 2: Process query results and prompt user
    // ========================================================================
    case "process-results": {
        const results = msg.payload;

        if (!results || results.length === 0) {
            node.warn("[RECOVERY] No session state found in database");
            return [null, null];
        }

        const session = results[0];

        // Check if there's a valid session to restore
        if (!session.work_order_id || session.cycle_count === 0) {
            node.warn("[RECOVERY] Session state exists but is empty - nothing to restore");
            return [null, null];
        }

        // Check if tracking was enabled
        if (!session.tracking_enabled) {
            node.warn("[RECOVERY] Previous session was not actively tracking - restoring state without prompt");

            // Restore the state silently
            global.set("cycleCount", session.cycle_count || 0);
            global.set("productionStartTime", session.production_start_time);
            global.set("operatingTime", Number(session.operating_time) || 0);
            global.set("downtime", Number(session.downtime) || 0);
            global.set("lastUpdateTime", session.last_update_time || Date.now());
            global.set("trackingEnabled", false);
            global.set("scrapPromptIssuedFor", session.scrap_prompt_issued_for || null);
            global.set("currentSessionId", session.current_session_id || null);

            return [null, null];
        }

        // Session was actively tracking - prompt user
        node.warn(`[RECOVERY] Found active session - Work Order: ${session.work_order_id}, Cycles: ${session.cycle_count}`);

        const promptMsg = {
            _mode: "recovery-prompt",
            recoveryPrompt: {
                workOrderId: session.work_order_id,
                cycleCount: session.cycle_count,
                operatingTime: Number(session.operating_time) || 0,
                downtime: Number(session.downtime) || 0,
                timestamp: session.last_update_time,
                sessionData: session
            }
        };

        return [promptMsg, null];
    }

    // ========================================================================
    // STEP 3: User chose to restore session
    // ========================================================================
    case "restore": {
        const sessionData = msg.payload;

        if (!sessionData) {
            node.error("[RECOVERY] No session data provided for restore");
            return [null, null];
        }

        node.warn(`[RECOVERY] Restoring session - Work Order: ${sessionData.work_order_id}, Cycles: ${sessionData.cycle_count}`);

        // Restore all global variables
        global.set("cycleCount", sessionData.cycle_count || 0);
        global.set("productionStartTime", sessionData.production_start_time);
        global.set("operatingTime", Number(sessionData.operating_time) || 0);
        global.set("downtime", Number(sessionData.downtime) || 0);
        global.set("lastUpdateTime", sessionData.last_update_time || Date.now());
        global.set("trackingEnabled", !!sessionData.tracking_enabled);
        global.set("scrapPromptIssuedFor", sessionData.scrap_prompt_issued_for || null);
        global.set("currentSessionId", sessionData.current_session_id || null);

        // Also need to restore activeWorkOrder from work_orders table
        const queryMsg = {
            _mode: "query-work-order",
            topic: `SELECT * FROM work_orders WHERE work_order_id = '${sessionData.work_order_id}';`
        };

        const notificationMsg = {
            _mode: "recovery-success",
            notification: {
                message: `Session restored: ${sessionData.work_order_id} - ${sessionData.cycle_count} cycles`,
                type: "success"
            }
        };

        return [notificationMsg, queryMsg];
    }

    // ========================================================================
    // STEP 4: Set activeWorkOrder from query result
    // ========================================================================
    case "set-work-order": {
        const results = msg.payload;

        if (!results || results.length === 0) {
            node.error("[RECOVERY] Work order not found in database");
            return [null, null];
        }

        const workOrder = results[0];

        // Reconstruct activeWorkOrder object
        const activeOrder = {
            id: workOrder.work_order_id,
            sku: workOrder.sku,
            target: workOrder.target,
            good: workOrder.good_parts,
            scrap: workOrder.scrap_count || 0,
            cycleTime: workOrder.cycle_time,
            theoreticalCycleTime: workOrder.cycle_time,
            progressPercent: workOrder.progress_percent,
            status: workOrder.status,
            lastUpdateIso: new Date().toISOString()
        };

        global.set("activeWorkOrder", activeOrder);

        node.warn(`[RECOVERY] Active work order restored: ${activeOrder.id}`);

        return [null, null];
    }

    // ========================================================================
    // STEP 5: User chose to start fresh
    // ========================================================================
    case "start-fresh": {
        node.warn("[RECOVERY] User chose to start fresh - clearing session state");

        // Clear all global variables
        global.set("cycleCount", 0);
        global.set("productionStartTime", null);
        global.set("operatingTime", 0);
        global.set("downtime", 0);
        global.set("lastUpdateTime", Date.now());
        global.set("trackingEnabled", false);
        global.set("activeWorkOrder", null);
        global.set("scrapPromptIssuedFor", null);
        global.set("currentSessionId", null);

        // Clear session_state in database
        const clearMsg = {
            _mode: "clear-session-state",
            topic: `
                UPDATE session_state
                SET
                    work_order_id = NULL,
                    cycle_count = 0,
                    production_start_time = NULL,
                    operating_time = 0,
                    downtime = 0,
                    last_update_time = NULL,
                    tracking_enabled = 0,
                    machine_state = 0,
                    scrap_prompt_issued_for = NULL,
                    current_session_id = NULL
                WHERE session_key = 'current_session';
            `
        };

        const notificationMsg = {
            _mode: "recovery-cleared",
            notification: {
                message: "Started with fresh session",
                type: "info"
            }
        };

        return [notificationMsg, clearMsg];
    }
}

// Default
return [null, null];


Good! Now I need to create UI components for the stop reason prompt. This will be HTML/JavaScript for Node-RED dashboard. Let me create a template for that.


TodoWrite

Review current Node-RED implementation structure

Reviewing current Node-RED implementation structure

Check existing database schema and tables

Checking existing database schema and tables

Analyze current flow logic for KPI tracking

Analyzing current flow logic for KPI tracking

Identify gaps vs optimization requirements

Identifying gaps vs optimization requirements

Create complete database migration script

Creating complete database migration script

Create enhanced Machine cycles function

Creating enhanced Machine cycles function

Create enhanced Work Order buttons function

Creating enhanced Work Order buttons function

Create startup recovery function

Create stop reason UI template

Create implementation guide document


Write

⚠ Production Stopped

Please select the reason for stopping production:

Planned Stops (will not affect downtime KPI)

                <button class="stop-reason-btn planned" data-category="planned" data-reason="Lunch break">
                    🍽 Lunch break
                </button>
                <button class="stop-reason-btn planned" data-category="planned" data-reason="Scheduled break">
                    ☕ Scheduled break
                </button>
                <button class="stop-reason-btn planned" data-category="planned" data-reason="Shift change">
                    🔄 Shift change
                </button>
                <button class="stop-reason-btn planned" data-category="planned" data-reason="Planned maintenance">
                    🔧 Planned maintenance
                </button>

Unplanned Stops (will affect availability KPI)

                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Machine malfunction">
                    ⚙ Machine malfunction
                </button>
                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Material shortage">
                    📦 Material shortage
                </button>
                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Quality issue">
                    ❌ Quality issue
                </button>
                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Operator error">
                    👤 Operator error
                </button>
                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Other">
                    ❓ Other
                </button>

Additional Notes (optional)

            <textarea id="stopReasonNotes" class="stop-reason-notes" placeholder="Enter any additional details..."></textarea>

            <button class="stop-reason-cancel" onclick="cancelStopReason()">Cancel</button>
            <button class="stop-reason-submit" id="submitStopReason" onclick="submitStopReason()" disabled>Submit</button>


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Review current Node-RED implementation structure

Check existing database schema and tables

Analyze current flow logic for KPI tracking

Identify gaps vs optimization requirements

Create complete database migration script

Create enhanced Machine cycles function

Create startup recovery function

Create stop reason UI components

Create implementation guide document

Create enhanced Machine cycles function

Create startup recovery function

Create stop reason UI template

Create implementation guide document


File created successfully at: /home/mdares/.node-red/stop_reason_ui_template.html

⚠ Production Stopped

Please select the reason for stopping production:

Planned Stops (will not affect downtime KPI)

                <button class="stop-reason-btn planned" data-category="planned" data-reason="Lunch break">
                    🍽 Lunch break
                </button>
                <button class="stop-reason-btn planned" data-category="planned" data-reason="Scheduled break">
                    ☕ Scheduled break
                </button>
                <button class="stop-reason-btn planned" data-category="planned" data-reason="Shift change">
                    🔄 Shift change
                </button>
                <button class="stop-reason-btn planned" data-category="planned" data-reason="Planned maintenance">
                    🔧 Planned maintenance
                </button>

Unplanned Stops (will affect availability KPI)

                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Machine malfunction">
                    ⚙ Machine malfunction
                </button>
                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Material shortage">
                    📦 Material shortage
                </button>
                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Quality issue">
                    ❌ Quality issue
                </button>
                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Operator error">
                    👤 Operator error
                </button>
                <button class="stop-reason-btn unplanned" data-category="unplanned" data-reason="Other">
                    ❓ Other
                </button>

Additional Notes (optional)

            <textarea id="stopReasonNotes" class="stop-reason-notes" placeholder="Enter any additional details..."></textarea>

            <button class="stop-reason-cancel" onclick="cancelStopReason()">Cancel</button>
            <button class="stop-reason-submit" id="submitStopReason" onclick="submitStopReason()" disabled>Submit</button>


Excellent! Now I need to create a comprehensive implementation guide that explains:1. What has been prepared
2. How to apply the changes to flows.json
3. Step-by-step instructions for deployment
4. Testing procedures

This will be the final piece that ties everything together for the user.


TodoWrite

Review current Node-RED implementation structure

Reviewing current Node-RED implementation structure

Check existing database schema and tables

Checking existing database schema and tables

Analyze current flow logic for KPI tracking

Analyzing current flow logic for KPI tracking

Identify gaps vs optimization requirements

Identifying gaps vs optimization requirements

Create complete database migration script

Creating complete database migration script

Create enhanced Machine cycles function

Creating enhanced Machine cycles function

Create enhanced Work Order buttons function

Creating enhanced Work Order buttons function

Create startup recovery function

Create stop reason UI template

Create implementation guide document