#!/usr/bin/env node /** * Complete Mock Server for SPORE UI * * This mock server provides a complete simulation of the SPORE embedded system * without requiring actual hardware or UDP port conflicts. It simulates: * - Multiple SPORE nodes with different IPs * - All API endpoints from the OpenAPI specification * - Discovery system without UDP conflicts * - Realistic data that changes over time * - Different scenarios (healthy, degraded, error states) */ const express = require('express'); const cors = require('cors'); const path = require('path'); const { getMockConfig } = require('./mock-configs'); // Load mock configuration const configName = process.env.MOCK_CONFIG || 'healthy'; const baseConfig = getMockConfig(configName); // Mock server configuration const MOCK_CONFIG = { // Server settings port: process.env.MOCK_PORT || 3002, baseUrl: process.env.MOCK_BASE_URL || 'http://localhost:3002', // Load configuration from preset ...baseConfig }; // Initialize Express app const app = express(); app.use(cors({ origin: true, credentials: true, allowedHeaders: ['Content-Type', 'Authorization'] })); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Mock data generators class MockDataGenerator { constructor() { this.startTime = Date.now(); this.nodeStates = new Map(); this.primaryNodeIndex = 0; this.initializeNodeStates(); } initializeNodeStates() { MOCK_CONFIG.nodes.forEach((node, index) => { this.nodeStates.set(node.ip, { ...node, freeHeap: this.generateFreeHeap(), uptime: 0, lastSeen: Date.now(), tasks: this.generateTasks(), systemInfo: this.generateSystemInfo(node), apiEndpoints: this.generateApiEndpoints() }); }); } generateFreeHeap() { // Simulate realistic ESP8266 memory usage const base = 30000; const variation = 20000; return Math.floor(base + Math.random() * variation); } generateSystemInfo(node) { return { freeHeap: this.generateFreeHeap(), chipId: node.chipId, sdkVersion: "3.1.2", cpuFreqMHz: 80, flashChipSize: 1048576 }; } generateTasks() { return [ { name: "discovery_send", interval: 1000, enabled: true, running: true, autoStart: true }, { name: "heartbeat", interval: 2000, enabled: true, running: true, autoStart: true }, { name: "status_update", interval: 1000, enabled: true, running: true, autoStart: true }, { name: "wifi_monitor", interval: 5000, enabled: true, running: Math.random() > 0.1, // 90% chance of running autoStart: true }, { name: "ota_check", interval: 30000, enabled: true, running: Math.random() > 0.2, // 80% chance of running autoStart: true }, { name: "cluster_sync", interval: 10000, enabled: true, running: Math.random() > 0.05, // 95% chance of running autoStart: true } ]; } generateApiEndpoints() { return [ { uri: "/api/node/status", method: "GET" }, { uri: "/api/tasks/status", method: "GET" }, { uri: "/api/tasks/control", method: "POST" }, { uri: "/api/cluster/members", method: "GET" }, { uri: "/api/node/update", method: "POST" }, { uri: "/api/node/restart", method: "POST" } ]; } updateNodeStates() { if (!MOCK_CONFIG.simulation.enableTimeProgression) return; this.nodeStates.forEach((nodeState, ip) => { // Update uptime nodeState.uptime = Date.now() - this.startTime; // Update free heap (simulate memory usage changes) const currentHeap = nodeState.freeHeap; const change = Math.floor((Math.random() - 0.5) * 1000); nodeState.freeHeap = Math.max(10000, currentHeap + change); // Update last seen nodeState.lastSeen = Date.now(); // Simulate random failures if (MOCK_CONFIG.simulation.enableRandomFailures && Math.random() < MOCK_CONFIG.simulation.failureRate) { nodeState.status = Math.random() > 0.5 ? 'INACTIVE' : 'DEAD'; } else { nodeState.status = 'ACTIVE'; } // Update task states nodeState.tasks.forEach(task => { if (task.enabled && Math.random() > 0.05) { // 95% chance of running when enabled task.running = true; } else { task.running = false; } }); }); // Rotate primary node if enabled if (MOCK_CONFIG.simulation.primaryNodeRotation) { this.primaryNodeIndex = (this.primaryNodeIndex + 1) % MOCK_CONFIG.nodes.length; } } getPrimaryNode() { return MOCK_CONFIG.nodes[this.primaryNodeIndex]; } getAllNodes() { return Array.from(this.nodeStates.values()); } getNodeByIp(ip) { return this.nodeStates.get(ip); } } // Initialize mock data generator const mockData = new MockDataGenerator(); // Update data periodically setInterval(() => { mockData.updateNodeStates(); }, MOCK_CONFIG.simulation.updateInterval); // API Routes // Health check endpoint app.get('/api/health', (req, res) => { const primaryNode = mockData.getPrimaryNode(); const allNodes = mockData.getAllNodes(); const activeNodes = allNodes.filter(node => node.status === 'ACTIVE'); const health = { status: activeNodes.length > 0 ? 'healthy' : 'degraded', timestamp: new Date().toISOString(), services: { http: true, udp: false, // Mock server doesn't use UDP sporeClient: true }, discovery: { totalNodes: allNodes.length, primaryNode: primaryNode.ip, udpPort: 4210, serverRunning: false // Mock server doesn't use UDP }, mock: { enabled: true, nodes: allNodes.length, activeNodes: activeNodes.length, simulationMode: MOCK_CONFIG.simulation.enableTimeProgression } }; if (activeNodes.length === 0) { health.status = 'degraded'; health.message = 'No active nodes in mock simulation'; } res.json(health); }); // Discovery endpoints (simulated) app.get('/api/discovery/nodes', (req, res) => { const primaryNode = mockData.getPrimaryNode(); const allNodes = mockData.getAllNodes(); const response = { primaryNode: primaryNode.ip, totalNodes: allNodes.length, clientInitialized: true, clientBaseUrl: `http://${primaryNode.ip}`, nodes: allNodes.map(node => ({ ip: node.ip, port: 80, discoveredAt: new Date(node.lastSeen - 60000).toISOString(), // 1 minute ago lastSeen: new Date(node.lastSeen).toISOString(), isPrimary: node.ip === primaryNode.ip, hostname: node.hostname, status: node.status })) }; res.json(response); }); app.post('/api/discovery/refresh', (req, res) => { // Simulate discovery refresh mockData.updateNodeStates(); res.json({ success: true, message: 'Discovery refresh completed', timestamp: new Date().toISOString() }); }); app.post('/api/discovery/primary/:ip', (req, res) => { const { ip } = req.params; const node = mockData.getNodeByIp(ip); if (!node) { return res.status(404).json({ success: false, message: `Node ${ip} not found` }); } // Find and set as primary const nodeIndex = MOCK_CONFIG.nodes.findIndex(n => n.ip === ip); if (nodeIndex !== -1) { mockData.primaryNodeIndex = nodeIndex; } res.json({ success: true, message: `Primary node set to ${ip}`, primaryNode: ip }); }); app.post('/api/discovery/random-primary', (req, res) => { const allNodes = mockData.getAllNodes(); const activeNodes = allNodes.filter(node => node.status === 'ACTIVE'); if (activeNodes.length === 0) { return res.status(503).json({ success: false, message: 'No active nodes available for selection' }); } // Randomly select a new primary const randomIndex = Math.floor(Math.random() * activeNodes.length); const newPrimary = activeNodes[randomIndex]; const nodeIndex = MOCK_CONFIG.nodes.findIndex(n => n.ip === newPrimary.ip); if (nodeIndex !== -1) { mockData.primaryNodeIndex = nodeIndex; } res.json({ success: true, message: `Primary node randomly selected: ${newPrimary.ip}`, primaryNode: newPrimary.ip, totalNodes: allNodes.length, clientInitialized: true }); }); // Task management endpoints app.get('/api/tasks/status', (req, res) => { const { ip } = req.query; let nodeData; if (ip) { nodeData = mockData.getNodeByIp(ip); if (!nodeData) { return res.status(404).json({ error: 'Node not found', message: `Node ${ip} not found in mock simulation` }); } } else { // Use primary node const primaryNode = mockData.getPrimaryNode(); nodeData = mockData.getNodeByIp(primaryNode.ip); } const tasks = nodeData.tasks; const activeTasks = tasks.filter(task => task.enabled && task.running).length; const response = { summary: { totalTasks: tasks.length, activeTasks: activeTasks }, tasks: tasks, system: { freeHeap: nodeData.freeHeap, uptime: nodeData.uptime } }; res.json(response); }); app.post('/api/tasks/control', (req, res) => { const { task, action } = req.body; if (!task || !action) { return res.status(400).json({ success: false, message: 'Missing parameters. Required: task, action', example: '{"task": "discovery_send", "action": "status"}' }); } const validActions = ['enable', 'disable', 'start', 'stop', 'status']; if (!validActions.includes(action)) { return res.status(400).json({ success: false, message: 'Invalid action. Use: enable, disable, start, stop, or status', task: task, action: action }); } // Simulate task control const primaryNode = mockData.getPrimaryNode(); const nodeData = mockData.getNodeByIp(primaryNode.ip); const taskData = nodeData.tasks.find(t => t.name === task); if (!taskData) { return res.status(404).json({ success: false, message: `Task ${task} not found` }); } // Apply action switch (action) { case 'enable': taskData.enabled = true; break; case 'disable': taskData.enabled = false; taskData.running = false; break; case 'start': if (taskData.enabled) { taskData.running = true; } break; case 'stop': taskData.running = false; break; case 'status': // Return detailed status return res.json({ success: true, message: 'Task status retrieved', task: task, action: action, taskDetails: { name: taskData.name, enabled: taskData.enabled, running: taskData.running, interval: taskData.interval, system: { freeHeap: nodeData.freeHeap, uptime: nodeData.uptime } } }); } res.json({ success: true, message: `Task ${action}d`, task: task, action: action }); }); // System status endpoint app.get('/api/node/status', (req, res) => { const { ip } = req.query; let nodeData; if (ip) { nodeData = mockData.getNodeByIp(ip); if (!nodeData) { return res.status(404).json({ error: 'Node not found', message: `Node ${ip} not found in mock simulation` }); } } else { // Use primary node const primaryNode = mockData.getPrimaryNode(); nodeData = mockData.getNodeByIp(primaryNode.ip); } const response = { freeHeap: nodeData.freeHeap, chipId: nodeData.chipId, sdkVersion: nodeData.systemInfo.sdkVersion, cpuFreqMHz: nodeData.systemInfo.cpuFreqMHz, flashChipSize: nodeData.systemInfo.flashChipSize, api: nodeData.apiEndpoints }; res.json(response); }); // Cluster members endpoint app.get('/api/cluster/members', (req, res) => { const allNodes = mockData.getAllNodes(); const members = allNodes.map(node => ({ hostname: node.hostname, ip: node.ip, lastSeen: Math.floor(node.lastSeen / 1000), // Convert to seconds latency: node.latency, status: node.status, resources: { freeHeap: node.freeHeap, chipId: node.chipId, sdkVersion: node.systemInfo.sdkVersion, cpuFreqMHz: node.systemInfo.cpuFreqMHz, flashChipSize: node.systemInfo.flashChipSize }, api: node.apiEndpoints })); res.json({ members }); }); // Node endpoints endpoint app.get('/api/node/endpoints', (req, res) => { const { ip } = req.query; let nodeData; if (ip) { nodeData = mockData.getNodeByIp(ip); if (!nodeData) { return res.status(404).json({ error: 'Node not found', message: `Node ${ip} not found in mock simulation` }); } } else { // Use primary node const primaryNode = mockData.getPrimaryNode(); nodeData = mockData.getNodeByIp(primaryNode.ip); } res.json(nodeData.apiEndpoints); }); // Generic proxy endpoint app.post('/api/proxy-call', (req, res) => { const { ip, method, uri, params } = req.body || {}; if (!ip || !method || !uri) { return res.status(400).json({ error: 'Missing required fields', message: 'Required: ip, method, uri' }); } // Simulate proxy call by routing to appropriate mock endpoint const nodeData = mockData.getNodeByIp(ip); if (!nodeData) { return res.status(404).json({ error: 'Node not found', message: `Node ${ip} not found in mock simulation` }); } // Simulate different responses based on URI if (uri === '/api/node/status') { return res.json({ freeHeap: nodeData.freeHeap, chipId: nodeData.chipId, sdkVersion: nodeData.systemInfo.sdkVersion, cpuFreqMHz: nodeData.systemInfo.cpuFreqMHz, flashChipSize: nodeData.systemInfo.flashChipSize, api: nodeData.apiEndpoints }); } else if (uri === '/api/tasks/status') { const tasks = nodeData.tasks; const activeTasks = tasks.filter(task => task.enabled && task.running).length; return res.json({ summary: { totalTasks: tasks.length, activeTasks: activeTasks }, tasks: tasks, system: { freeHeap: nodeData.freeHeap, uptime: nodeData.uptime } }); } else if (uri === '/api/monitoring/resources') { // Return realistic monitoring resources data const totalHeap = nodeData.systemInfo.flashChipSize || 1048576; // 1MB default const freeHeap = nodeData.freeHeap; const usedHeap = totalHeap - freeHeap; const heapUsagePercent = (usedHeap / totalHeap) * 100; return res.json({ cpu: { average_usage: Math.random() * 30 + 10, // 10-40% CPU usage current_usage: Math.random() * 50 + 5, // 5-55% current usage frequency_mhz: nodeData.systemInfo.cpuFreqMHz || 80 }, memory: { total_heap: totalHeap, free_heap: freeHeap, used_heap: usedHeap, heap_usage_percent: heapUsagePercent, min_free_heap: Math.floor(freeHeap * 0.8), // 80% of current free heap max_alloc_heap: Math.floor(totalHeap * 0.9) // 90% of total heap }, filesystem: { total_bytes: 3145728, // 3MB SPIFFS used_bytes: Math.floor(3145728 * (0.3 + Math.random() * 0.4)), // 30-70% used free_bytes: 0 // Will be calculated }, network: { wifi_rssi: -30 - Math.floor(Math.random() * 40), // -30 to -70 dBm wifi_connected: true, uptime_seconds: nodeData.uptime }, timestamp: new Date().toISOString() }); } else { return res.json({ success: true, message: `Mock response for ${method} ${uri}`, node: ip, timestamp: new Date().toISOString() }); } }); // Firmware update endpoint app.post('/api/node/update', (req, res) => { // Simulate firmware update res.json({ status: 'updating', message: 'Firmware update in progress (mock simulation)' }); }); // System restart endpoint app.post('/api/node/restart', (req, res) => { // Simulate system restart res.json({ status: 'restarting' }); }); // Test route app.get('/test', (req, res) => { res.send('Mock server is working!'); }); // Serve the mock UI (main UI with modified API client) app.get('/', (req, res) => { const filePath = path.join(__dirname, 'mock-ui.html'); console.log('Serving mock UI from:', filePath); res.sendFile(filePath); }); // Serve the original mock frontend app.get('/frontend', (req, res) => { res.sendFile(path.join(__dirname, 'mock-frontend.html')); }); // Serve the main UI with modified API client app.get('/ui', (req, res) => { res.sendFile(path.join(__dirname, '../public/index.html')); }); // Serve static files from public directory (after custom routes) // Only serve static files for specific paths, not the root app.use('/static', express.static(path.join(__dirname, '../public'))); app.use('/styles', express.static(path.join(__dirname, '../public/styles'))); app.use('/scripts', express.static(path.join(__dirname, '../public/scripts'))); app.use('/vendor', express.static(path.join(__dirname, '../public/vendor'))); // Serve mock API client app.get('/test/mock-api-client.js', (req, res) => { res.sendFile(path.join(__dirname, 'mock-api-client.js')); }); // Serve test page app.get('/test-page', (req, res) => { res.sendFile(path.join(__dirname, 'test-page.html')); }); // Serve favicon to prevent 404 errors app.get('/favicon.ico', (req, res) => { res.status(204).end(); // No content }); // Serve mock server info page app.get('/info', (req, res) => { res.send(`
Status: Running
Port: ${MOCK_CONFIG.port}
Configuration: ${MOCK_CONFIG.name}
Mock Nodes: ${MOCK_CONFIG.nodes.length}
Primary Node: ${mockData.getPrimaryNode().ip}
Mock UI (Port 3002) - Full UI with mock data
Mock Frontend - Custom mock frontend
Real UI (Port 3002) - Real UI connected to mock server
API Health - Check server status
Use npm scripts to change configuration:
npm run mock:healthy - Healthy cluster (3 nodes)npm run mock:degraded - Degraded cluster (some inactive)npm run mock:large - Large cluster (8 nodes)npm run mock:unstable - Unstable cluster (high failure rate)npm run mock:single - Single nodenpm run mock:empty - Empty cluster