847 lines
25 KiB
JavaScript
847 lines
25 KiB
JavaScript
#!/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" },
|
|
{
|
|
uri: "/api/led/brightness",
|
|
method: "POST",
|
|
params: [
|
|
{
|
|
name: "brightness",
|
|
type: "numberRange",
|
|
location: "body",
|
|
required: true,
|
|
value: 255,
|
|
default: 128
|
|
}
|
|
]
|
|
},
|
|
{
|
|
uri: "/api/led/color",
|
|
method: "POST",
|
|
params: [
|
|
{
|
|
name: "color",
|
|
type: "color",
|
|
location: "body",
|
|
required: true,
|
|
default: 16711680
|
|
}
|
|
]
|
|
},
|
|
{
|
|
uri: "/api/sensor/interval",
|
|
method: "POST",
|
|
params: [
|
|
{
|
|
name: "interval",
|
|
type: "numberRange",
|
|
location: "body",
|
|
required: true,
|
|
value: 10000,
|
|
default: 1000
|
|
}
|
|
]
|
|
},
|
|
{
|
|
uri: "/api/system/mode",
|
|
method: "POST",
|
|
params: [
|
|
{
|
|
name: "mode",
|
|
type: "string",
|
|
location: "body",
|
|
required: true,
|
|
values: ["normal", "debug", "maintenance"],
|
|
default: "normal"
|
|
}
|
|
]
|
|
}
|
|
];
|
|
}
|
|
|
|
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(`
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SPORE UI - Mock Server Info</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
|
|
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
h1 { color: #333; text-align: center; }
|
|
.status { background: #e8f5e8; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
|
.info { background: #f0f8ff; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
|
.endpoint { background: #f9f9f9; padding: 10px; margin: 5px 0; border-left: 4px solid #007acc; }
|
|
.mock-note { background: #fff3cd; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #ffc107; }
|
|
.btn { display: inline-block; padding: 10px 20px; background: #007acc; color: white; text-decoration: none; border-radius: 5px; margin: 5px; }
|
|
.btn:hover { background: #005a9e; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🚀 SPORE UI Mock Server</h1>
|
|
|
|
<div class="mock-note">
|
|
<strong>Mock Mode Active:</strong> This is a complete simulation of the SPORE embedded system.
|
|
No real hardware or UDP ports are required.
|
|
</div>
|
|
|
|
<div class="status">
|
|
<h3>📊 Server Status</h3>
|
|
<p><strong>Status:</strong> Running</p>
|
|
<p><strong>Port:</strong> ${MOCK_CONFIG.port}</p>
|
|
<p><strong>Configuration:</strong> ${MOCK_CONFIG.name}</p>
|
|
<p><strong>Mock Nodes:</strong> ${MOCK_CONFIG.nodes.length}</p>
|
|
<p><strong>Primary Node:</strong> ${mockData.getPrimaryNode().ip}</p>
|
|
</div>
|
|
|
|
<div class="info">
|
|
<h3>🌐 Access Points</h3>
|
|
<p><a href="/" class="btn">Mock UI (Port 3002)</a> - Full UI with mock data</p>
|
|
<p><a href="/frontend" class="btn">Mock Frontend</a> - Custom mock frontend</p>
|
|
<p><a href="/ui" class="btn">Real UI (Port 3002)</a> - Real UI connected to mock server</p>
|
|
<p><a href="/api/health" class="btn">API Health</a> - Check server status</p>
|
|
</div>
|
|
|
|
<div class="info">
|
|
<h3>🔗 Available Endpoints</h3>
|
|
<div class="endpoint"><strong>GET</strong> /api/health - Health check</div>
|
|
<div class="endpoint"><strong>GET</strong> /api/discovery/nodes - Discovery status</div>
|
|
<div class="endpoint"><strong>GET</strong> /api/tasks/status - Task status</div>
|
|
<div class="endpoint"><strong>POST</strong> /api/tasks/control - Control tasks</div>
|
|
<div class="endpoint"><strong>GET</strong> /api/node/status - System status</div>
|
|
<div class="endpoint"><strong>GET</strong> /api/cluster/members - Cluster members</div>
|
|
<div class="endpoint"><strong>POST</strong> /api/proxy-call - Generic proxy</div>
|
|
</div>
|
|
|
|
<div class="info">
|
|
<h3>🎮 Mock Features</h3>
|
|
<ul>
|
|
<li>✅ Multiple simulated SPORE nodes</li>
|
|
<li>✅ Realistic data that changes over time</li>
|
|
<li>✅ No UDP port conflicts</li>
|
|
<li>✅ All API endpoints implemented</li>
|
|
<li>✅ Random failures simulation</li>
|
|
<li>✅ Primary node rotation</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info">
|
|
<h3>🔧 Configuration</h3>
|
|
<p>Use npm scripts to change configuration:</p>
|
|
<ul>
|
|
<li><code>npm run mock:healthy</code> - Healthy cluster (3 nodes)</li>
|
|
<li><code>npm run mock:degraded</code> - Degraded cluster (some inactive)</li>
|
|
<li><code>npm run mock:large</code> - Large cluster (8 nodes)</li>
|
|
<li><code>npm run mock:unstable</code> - Unstable cluster (high failure rate)</li>
|
|
<li><code>npm run mock:single</code> - Single node</li>
|
|
<li><code>npm run mock:empty</code> - Empty cluster</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`);
|
|
});
|
|
|
|
// Start the mock server
|
|
const server = app.listen(MOCK_CONFIG.port, () => {
|
|
console.log('🚀 SPORE UI Mock Server Started');
|
|
console.log('================================');
|
|
console.log(`Configuration: ${MOCK_CONFIG.name}`);
|
|
console.log(`Description: ${MOCK_CONFIG.description}`);
|
|
console.log(`Port: ${MOCK_CONFIG.port}`);
|
|
console.log(`URL: http://localhost:${MOCK_CONFIG.port}`);
|
|
console.log(`Mock Nodes: ${MOCK_CONFIG.nodes.length}`);
|
|
console.log(`Primary Node: ${mockData.getPrimaryNode().ip}`);
|
|
console.log('');
|
|
console.log('📡 Available Mock Nodes:');
|
|
MOCK_CONFIG.nodes.forEach((node, index) => {
|
|
console.log(` ${index + 1}. ${node.hostname} (${node.ip}) - ${node.status}`);
|
|
});
|
|
console.log('');
|
|
console.log('🎮 Mock Features:');
|
|
console.log(' ✅ No UDP port conflicts');
|
|
console.log(' ✅ Realistic data simulation');
|
|
console.log(' ✅ All API endpoints');
|
|
console.log(` ✅ Time-based data updates (${MOCK_CONFIG.simulation.updateInterval}ms)`);
|
|
console.log(` ✅ Random failure simulation (${MOCK_CONFIG.simulation.enableRandomFailures ? 'Enabled' : 'Disabled'})`);
|
|
console.log(` ✅ Primary node rotation (${MOCK_CONFIG.simulation.primaryNodeRotation ? 'Enabled' : 'Disabled'})`);
|
|
console.log('');
|
|
console.log('Press Ctrl+C to stop');
|
|
});
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGINT', () => {
|
|
console.log('\n\n👋 Mock server stopped. Goodbye!');
|
|
server.close(() => {
|
|
process.exit(0);
|
|
});
|
|
});
|
|
|
|
module.exports = { app, mockData, MOCK_CONFIG };
|