Fix connector start dates for SH-Meters and XMeters
Updated hardcoded dates from 2025 to 2026: - SH-Meters: 2026-01-12 - XMeters: 2026-01-25 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
330
water-api/src/routes/system.routes.ts
Normal file
330
water-api/src/routes/system.routes.ts
Normal file
@@ -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<string, Date> = {
|
||||||
|
'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;
|
||||||
Reference in New Issue
Block a user