diff --git a/water-api/src/routes/system.routes.ts b/water-api/src/routes/system.routes.ts new file mode 100644 index 0000000..abb0d37 --- /dev/null +++ b/water-api/src/routes/system.routes.ts @@ -0,0 +1,330 @@ +import { Router, Response } from 'express'; +import os from 'os'; +import { authenticateToken, requireRole } from '../middleware/auth.middleware'; +import { AuthenticatedRequest } from '../types'; +import pool from '../config/database'; + +const router = Router(); + +// Track request metrics (in-memory for simplicity) +let requestMetrics = { + total: 0, + errors: 0, + totalResponseTime: 0, +}; + +// Middleware to track requests (exported for use in main app) +export function trackRequest(responseTime: number, isError: boolean) { + requestMetrics.total++; + requestMetrics.totalResponseTime += responseTime; + if (isError) { + requestMetrics.errors++; + } +} + +/** + * GET /api/system/metrics + * Get server metrics (Admin only) + */ +router.get( + '/metrics', + authenticateToken, + requireRole('ADMIN'), + async (_req: AuthenticatedRequest, res: Response) => { + try { + // Test database connection + let dbConnected = false; + let dbResponseTime = 0; + + try { + const startTime = Date.now(); + await pool.query('SELECT 1'); + dbResponseTime = Date.now() - startTime; + dbConnected = true; + } catch { + dbConnected = false; + } + + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + + const metrics = { + uptime: process.uptime(), + memory: { + total: totalMem, + used: usedMem, + free: freeMem, + percentage: (usedMem / totalMem) * 100, + }, + cpu: { + usage: os.loadavg()[0] * 10, // Approximate CPU percentage from load average + cores: os.cpus().length, + }, + requests: { + total: requestMetrics.total, + errors: requestMetrics.errors, + avgResponseTime: requestMetrics.total > 0 + ? requestMetrics.totalResponseTime / requestMetrics.total + : 0, + }, + database: { + connected: dbConnected, + responseTime: dbResponseTime, + }, + timestamp: new Date().toISOString(), + }; + + res.json({ + success: true, + data: metrics, + }); + } catch (error) { + console.error('Error getting system metrics:', error); + res.status(500).json({ + success: false, + error: 'Failed to get system metrics', + }); + } + } +); + +/** + * GET /api/system/health + * Detailed health check (Admin only) + */ +router.get( + '/health', + authenticateToken, + requireRole('ADMIN'), + async (_req: AuthenticatedRequest, res: Response) => { + try { + let dbConnected = false; + + try { + await pool.query('SELECT 1'); + dbConnected = true; + } catch { + dbConnected = false; + } + + res.json({ + success: true, + data: { + status: dbConnected ? 'healthy' : 'degraded', + database: dbConnected, + uptime: process.uptime(), + }, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: 'Health check failed', + }); + } + } +); + +/** + * GET /api/system/meters-locations + * Get meters with coordinates for map (Admin only) + */ +router.get( + '/meters-locations', + authenticateToken, + requireRole('ADMIN'), + async (_req: AuthenticatedRequest, res: Response) => { + try { + // Query meters with their coordinates and latest reading + const result = await pool.query(` + SELECT + m.id, + m.serial_number, + m.name, + m.status, + p.name as project_name, + m.latitude as lat, + m.longitude as lng, + m.last_reading_value as last_reading, + m.last_reading_at as last_reading_date + FROM meters m + LEFT JOIN projects p ON m.project_id = p.id + WHERE m.latitude IS NOT NULL AND m.longitude IS NOT NULL + ORDER BY m.name + `); + + res.json({ + success: true, + data: result.rows, + }); + } catch (error) { + console.error('Error getting meter locations:', error); + res.status(500).json({ + success: false, + error: 'Failed to get meter locations', + }); + } + } +); + +/** + * GET /api/system/report-stats + * Get statistics for reports dashboard (Admin only) + */ +router.get( + '/report-stats', + authenticateToken, + requireRole('ADMIN'), + async (_req: AuthenticatedRequest, res: Response) => { + try { + // Get meter counts + const meterCountsResult = await pool.query(` + SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE status = 'active') as active, + COUNT(*) FILTER (WHERE status != 'active') as inactive + FROM meters + `); + + // Get total consumption from meters (last_reading_value) + const consumptionResult = await pool.query(` + SELECT COALESCE(SUM(last_reading_value), 0) as total_consumption + FROM meters + WHERE last_reading_value IS NOT NULL + `); + + // Get project count + const projectCountResult = await pool.query(` + SELECT COUNT(*) as total FROM projects + `); + + // Get meters with alerts (negative flow) + const alertsResult = await pool.query(` + SELECT COUNT(*) as count + FROM meters + WHERE current_flow < 0 OR total_flow_reverse > 0 + `); + + // Get consumption by project + const consumptionByProjectResult = await pool.query(` + SELECT + p.name as project_name, + COALESCE(SUM(m.last_reading_value), 0) as total_consumption, + COUNT(m.id) as meter_count + FROM projects p + LEFT JOIN meters m ON m.project_id = p.id + GROUP BY p.id, p.name + ORDER BY total_consumption DESC + LIMIT 10 + `); + + // Get consumption trend (last 6 months from meter_readings) + let trendResult = { rows: [] as any[] }; + try { + trendResult = await pool.query(` + SELECT + TO_CHAR(DATE_TRUNC('month', received_at), 'YYYY-MM') as date, + SUM(reading_value) as consumption + FROM meter_readings + WHERE received_at >= NOW() - INTERVAL '6 months' + GROUP BY DATE_TRUNC('month', received_at) + ORDER BY date + `); + } catch (e) { + // meter_readings table might not exist, use empty array + console.log('meter_readings query failed, using empty trend data'); + } + + const meterCounts = meterCountsResult.rows[0]; + + res.json({ + success: true, + data: { + totalMeters: parseInt(meterCounts.total) || 0, + activeMeters: parseInt(meterCounts.active) || 0, + inactiveMeters: parseInt(meterCounts.inactive) || 0, + totalConsumption: parseFloat(consumptionResult.rows[0]?.total_consumption) || 0, + totalProjects: parseInt(projectCountResult.rows[0]?.total) || 0, + metersWithAlerts: parseInt(alertsResult.rows[0]?.count) || 0, + consumptionByProject: consumptionByProjectResult.rows.map(row => ({ + project_name: row.project_name, + total_consumption: parseFloat(row.total_consumption) || 0, + meter_count: parseInt(row.meter_count) || 0, + })), + consumptionTrend: trendResult.rows.map(row => ({ + date: row.date, + consumption: parseFloat(row.consumption) || 0, + })), + }, + }); + } catch (error) { + console.error('Error getting report stats:', error); + res.status(500).json({ + success: false, + error: 'Failed to get report statistics', + }); + } + } +); + +/** + * GET /api/system/connector-stats/:type + * Get connector statistics (Admin only) + */ +router.get( + '/connector-stats/:type', + authenticateToken, + requireRole('ADMIN'), + async (req: AuthenticatedRequest, res: Response) => { + try { + const { type } = req.params; + + let meterType = ''; + if (type === 'sh-meters') { + meterType = 'LORA'; + } else if (type === 'xmeters') { + meterType = 'GRANDES'; + } + + // Get meter count by type + const meterCountResult = await pool.query( + `SELECT COUNT(*) as count FROM meters WHERE UPPER(type) = $1`, + [meterType] + ); + + const meterCount = parseInt(meterCountResult.rows[0]?.count) || 0; + + // Start dates for each connector + const connectorStartDates: Record = { + 'sh-meters': new Date('2026-01-12'), + 'xmeters': new Date('2026-01-25'), + }; + + const startDate = connectorStartDates[type] || new Date(); + const today = new Date(); + const diffTime = Math.abs(today.getTime() - startDate.getTime()); + const daysSinceStart = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + // Messages = meters * days (one message per meter per day) + const messagesReceived = meterCount * daysSinceStart; + + res.json({ + success: true, + data: { + meterCount, + messagesReceived, + daysSinceStart, + meterType, + }, + }); + } catch (error) { + console.error('Error getting connector stats:', error); + res.status(500).json({ + success: false, + error: 'Failed to get connector statistics', + }); + } + } +); + +export default router;