tcp-serial-relay
Version:
Modular TCP to Serial relay service with comprehensive logging, monitoring and web dashboard
343 lines (306 loc) • 10.4 kB
JavaScript
// Fixed dashboard/server.js - Standalone dashboard server
const express = require('express');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { SerialPort } = require('serialport');
class DashboardServer {
constructor(options = {}) {
this.port = options.port || process.env.DASHBOARD_PORT || process.env.PORT || 3000;
this.app = express();
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
// Enable CORS for API access
this.app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
this.app.use(express.json());
this.app.use(express.static(path.join(__dirname, 'public')));
}
setupRoutes() {
// API Routes
this.app.get('/api/status', this.getStatus.bind(this));
this.app.get('/api/config', this.getConfig.bind(this));
this.app.post('/api/config', this.updateConfig.bind(this));
this.app.get('/api/logs', this.getLogs.bind(this));
this.app.get('/api/health', this.getHealth.bind(this));
this.app.get('/api/ports', this.getPorts.bind(this));
this.app.post('/api/control/:action', this.controlService.bind(this));
// Serve main dashboard for all other routes
this.app.get('*', (req, res) => {
const indexPath = path.join(__dirname, 'public', 'index.html');
if (fs.existsSync(indexPath)) {
res.sendFile(indexPath);
} else {
res.status(404).send(`
<h1>Dashboard Not Found</h1>
<p>Dashboard static files not found at: ${indexPath}</p>
<p>Please run the build script to generate dashboard files.</p>
`);
}
});
}
async getStatus(req, res) {
try {
// Try to read latest status from status directory
const statusDirs = [
process.env.STATUS_DIR,
'/opt/tcp-serial-relay/status',
path.join(process.cwd(), 'status'),
path.join(__dirname, '..', '..', 'status')
].filter(Boolean);
let latestStatus = null;
for (const statusDir of statusDirs) {
if (fs.existsSync(statusDir)) {
const statusFiles = fs.readdirSync(statusDir)
.filter(f => f.startsWith('status-') && f.endsWith('.json'))
.sort()
.reverse();
if (statusFiles.length > 0) {
const statusFile = path.join(statusDir, statusFiles[0]);
try {
latestStatus = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
break;
} catch (error) {
console.warn('Error reading status file:', error.message);
}
}
}
}
const status = {
timestamp: new Date().toISOString(),
overall: latestStatus?.success ? 'HEALTHY' : 'UNKNOWN',
success: latestStatus?.success || false,
message: latestStatus?.message || 'No recent status available',
duration: latestStatus?.duration || 0,
connections: latestStatus?.connections || {
tcp: { connected: false },
secondary: { connected: false }
},
metrics: latestStatus?.metrics || {
dataTransfers: 0,
errors: 0,
bytesTransferredTcpToSerial: 0,
bytesTransferredSerialToTcp: 0,
bytesTransferredTcpToSecondaryTcp: 0,
bytesTransferredSecondaryTcpToTcp: 0
},
systemMetrics: {
uptime: os.uptime(),
totalMemory: os.totalmem(),
freeMemory: os.freemem(),
cpuUsage: 0,
loadAvg: os.loadavg()
}
};
res.json({ status });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getConfig(req, res) {
try {
const configPaths = [
process.env.CONFIG_PATH,
'/etc/tcp-serial-relay/relay-config.json',
path.join(process.cwd(), 'config', 'relay-config.json'),
path.join(__dirname, '..', '..', 'config', 'relay-config.json')
].filter(Boolean);
let config = {};
for (const configPath of configPaths) {
if (fs.existsSync(configPath)) {
try {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
break;
} catch (error) {
console.warn('Error reading config file:', error.message);
}
}
}
res.json({ config });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async updateConfig(req, res) {
try {
const newConfig = req.body;
const configPath = process.env.CONFIG_PATH ||
path.join(process.cwd(), 'config', 'relay-config.json');
// Ensure config directory exists
const configDir = path.dirname(configPath);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
res.json({ success: true, message: 'Configuration updated successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getLogs(req, res) {
try {
const lines = parseInt(req.query.lines) || 50;
const logs = [];
// Try to read from various log locations
const logDirs = [
'/var/log/tcp-serial-relay',
path.join(process.cwd(), 'logs'),
path.join(__dirname, '..', '..', 'logs')
];
let foundLogs = false;
for (const logDir of logDirs) {
if (fs.existsSync(logDir)) {
const logFiles = fs.readdirSync(logDir)
.filter(f => f.endsWith('.log'))
.sort()
.reverse();
if (logFiles.length > 0) {
const logFile = path.join(logDir, logFiles[0]);
try {
const content = fs.readFileSync(logFile, 'utf8');
const logLines = content.split('\n')
.filter(line => line.trim())
.slice(-lines)
.map((line, index) => {
const match = line.match(/^(\S+\s+\S+)\s+\[(\w+)\]:\s+(.+)$/);
if (match) {
return {
timestamp: match[1],
level: match[2],
message: match[3]
};
}
return {
timestamp: new Date().toISOString(),
level: 'INFO',
message: line
};
});
logs.push(...logLines);
foundLogs = true;
break;
} catch (error) {
console.warn('Error reading log file:', error.message);
}
}
}
}
if (!foundLogs) {
// Generate sample logs if no real logs found
for (let i = 0; i < Math.min(lines, 10); i++) {
logs.push({
timestamp: new Date(Date.now() - i * 60000).toISOString(),
level: 'INFO',
message: `No log files found in standard locations (sample entry ${i + 1})`
});
}
}
res.json({ logs: logs.reverse() });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getHealth(req, res) {
try {
const health = {
status: 'unknown',
uptime: process.uptime() * 1000,
connections: {
tcp: { connected: false },
secondary: { connected: false }
},
metrics: {
dataTransfers: 0,
errors: 0
}
};
res.json(health);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getPorts(req, res) {
try {
const ports = await SerialPort.list();
res.json({ ports });
} catch (error) {
console.warn('Error listing serial ports:', error.message);
res.json({ ports: [], error: 'Could not list serial ports' });
}
}
async controlService(req, res) {
try {
const action = req.params.action;
const { spawn } = require('child_process');
switch (action) {
case 'start':
// This would typically interface with the actual service
res.json({ success: true, message: 'Start command received (dashboard-only mode)' });
break;
case 'stop':
res.json({ success: true, message: 'Stop command received (dashboard-only mode)' });
break;
case 'restart':
res.json({ success: true, message: 'Restart command received (dashboard-only mode)' });
break;
default:
res.status(400).json({ error: 'Unknown action' });
}
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async start() {
return new Promise((resolve, reject) => {
this.server = this.app.listen(this.port, '0.0.0.0', (error) => {
if (error) {
reject(error);
} else {
console.log(`✅ Dashboard server started successfully`);
console.log(`🌐 Dashboard URL: http://localhost:${this.port}`);
console.log(`📁 Static files: ${path.join(__dirname, 'public')}`);
resolve();
}
});
});
}
async stop() {
return new Promise((resolve) => {
if (this.server) {
this.server.close(() => {
console.log('Dashboard server stopped');
resolve();
});
} else {
resolve();
}
});
}
}
// If run directly, start the server
if (require.main === module) {
const server = new DashboardServer();
server.start().catch((error) => {
console.error('Failed to start dashboard server:', error);
process.exit(1);
});
// Graceful shutdown
const shutdown = async (signal) => {
console.log(`\nReceived ${signal}. Shutting down dashboard...`);
await server.stop();
process.exit(0);
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}
module.exports = DashboardServer;