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