2 * @file routes/dashboard.js
3 * @module routes/dashboard
4 * @description Dashboard statistics and metrics API for MSP overview. Provides real-time agent
5 * health metrics, business KPIs, and aggregated statistics across ticket, billing, monitoring,
6 * and alerting systems.
8 * **Dashboard Components:**
9 * - Agent health: Real-time CPU, memory, disk, uptime from RMM agents
10 * - Ticket metrics: Active tickets, closed this month
11 * - Alerting: Unread critical alerts
12 * - Monitoring: Online agent count, device health
13 * - Financial: Invoices and payments for current month
14 * - Incidents: Recent critical incidents/alerts
16 * **Agent Metrics Collection:**
17 * Retrieves most recent metrics for each agent using PostgreSQL DISTINCT ON for efficiency.
18 * Merges agent metadata (hostname, OS, customer) with latest metric snapshot.
20 * **Time-Based Filtering:**
21 * - Active tickets: status IN ('open', 'in_progress')
22 * - Online agents: last_seen within 5 minutes
23 * - Monthly stats: DATE_TRUNC('month') for current month aggregations
24 * - Paid invoices: status = 'paid' within current month
26 * **Multi-Tenant Isolation:**
27 * All queries filtered by authenticated user's tenant scope. Root MSP users see all tenants'
28 * data. Regular users see only their tenant's dashboard.
30 * **Performance Considerations:**
31 * - Uses COUNT(*)::int for efficient counting
32 * - DISTINCT ON for latest metric per agent (avoids window functions)
33 * - Separate queries for each metric (parallelizable by client)
34 * - Limited incident list (5 most recent)
37 * - Homepage dashboard overview
38 * - Real-time monitoring dashboards
39 * - Executive reporting
40 * - Health check status boards
41 * - MSP portal home page
43 * @requires ../services/db
44 * @requires ../middleware/auth
45 * @requires ../middleware/tenant
46 * @author IBG MSP Development Team
49 * @see {@link module:routes/agent} for agent management
50 * @see {@link module:routes/tickets} for ticketing system
51 * @see {@link module:routes/invoices} for billing
54const express = require('express');
55const router = express.Router();
56const pool = require('../services/db');
57const authenticateToken = require('../middleware/auth');
58const { getTenantFilter, setTenantContext } = require('../middleware/tenant');
60// Apply authentication and tenant context to all routes
61router.use(authenticateToken, setTenantContext);
66 * @api {get} /dashboard/agent-metrics Get Agent Metrics
67 * @apiName GetAgentMetrics
69 * @apiDescription Retrieves latest system metrics for all RMM agents in tenant. Combines agent
70 * metadata (hostname, OS, customer) with most recent metric snapshot (CPU, memory, disk, uptime).
71 * Used for real-time monitoring dashboards and health overviews.
73 * **Metrics Included:**
74 * - CPU: CPU usage percentage (0-100)
75 * - Memory: Memory usage percentage (0-100)
76 * - Disk: Disk usage percentage (0-100)
77 * - Uptime: System uptime in seconds
78 * - Raw: Complete raw metrics JSON from agent
81 * Returns most recent metric for each agent using PostgreSQL DISTINCT ON optimization.
82 * Metric age depends on agent heartbeat interval (typically 1-5 minutes).
85 * - metrics: non-null - Agent reporting metrics
86 * - metrics: null - Agent never reported metrics or data purged
88 * **Tenant Filtering:**
89 * Only returns agents belonging to authenticated user's tenant. Root MSP users see all
91 * @apiHeader {string} Authorization Bearer JWT token
92 * @apiSuccess {object[]} agents Array of agents with latest metrics
93 * @apiSuccess {number} agents.agent_id Agent ID
94 * @apiSuccess {string} agents.agent_uuid Agent UUID
95 * @apiSuccess {string} agents.hostname Device hostname
96 * @apiSuccess {string} agents.os Operating system (Windows, Linux, macOS)
97 * @apiSuccess {number} agents.customer_id Associated customer ID
98 * @apiSuccess {object} agents.metrics Latest metric snapshot (or null)
99 * @apiSuccess {number} agents.metrics.agent_id Agent ID
100 * @apiSuccess {string} agents.metrics.ts Metric timestamp
101 * @apiSuccess {number} agents.metrics.cpu CPU usage percentage
102 * @apiSuccess {number} agents.metrics.memory Memory usage percentage
103 * @apiSuccess {number} agents.metrics.disk Disk usage percentage
104 * @apiSuccess {number} agents.metrics.uptime System uptime in seconds
105 * @apiSuccess {Object} agents.metrics.raw Raw metrics JSON from agent
106 * @apiError {number} 500 Failed to fetch agent metrics
107 * @apiExample {curl} Example Request:
109 * -H "Authorization: Bearer eyJhbGc..." \
110 * "https://api.everydaytech.au/dashboard/agent-metrics"
111 * @apiExample {json} Success Response:
117 * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000",
118 * "hostname": "ACME-DESKTOP-01",
119 * "os": "Windows 11 Pro",
123 * "ts": "2024-03-11T17:58:00.000Z",
128 * "raw": {"cpu_cores": 8, "memory_total_gb": 32, "disk_total_gb": 512}
133 * "agent_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
134 * "hostname": "ACME-SERVER-01",
135 * "os": "Ubuntu 22.04 LTS",
143 * @see {@link module:routes/agent} for agent management
144 * @see {@link module:routes/dashboard.getStats} for dashboard statistics
146router.get('/agent-metrics', async (req, res) => {
148 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req);
149 const whereClause = tenantClause ? `WHERE ${tenantClause}` : '';
150 // Get all agents for tenant
151 const agentsResult = await pool.query(
152 `SELECT agent_id, agent_uuid, hostname, os, customer_id FROM agents ${whereClause}`,
155 const agents = agentsResult.rows;
156 // For each agent, get latest metric
157 const metricsResult = await pool.query(
158 `SELECT DISTINCT ON (agent_id) agent_id, ts, cpu, memory, disk, uptime, raw
160 WHERE agent_id IN (${agents.map(a => a.agent_id).join(',') || 'NULL'})
161 ORDER BY agent_id, ts DESC`
163 const metricsByAgent = {};
164 metricsResult.rows.forEach(m => { metricsByAgent[m.agent_id] = m; });
165 // Merge agent info and metrics
166 const result = agents.map(agent => ({
167 agent_id: agent.agent_id,
168 agent_uuid: agent.agent_uuid,
169 hostname: agent.hostname,
171 customer_id: agent.customer_id,
172 metrics: metricsByAgent[agent.agent_id] || null
174 res.json({ agents: result });
176 console.error('Error fetching agent metrics:', err);
177 res.status(500).json({ error: 'Failed to fetch agent metrics' });
182 * @api {get} /dashboard/stats Get Dashboard Statistics
183 * @apiName GetDashboardStats
184 * @apiGroup Dashboard
185 * @apiDescription Retrieves comprehensive dashboard statistics and KPIs for MSP overview.
186 * Aggregates data across ticketing, monitoring, billing, and alerting systems. All metrics
187 * filtered by tenant scope.
189 * **Statistics Provided:**
190 * - activeTickets: Open or in-progress tickets
191 * - criticalAlerts: Unread alert-type notifications
192 * - ticketsClosedThisMonth: Tickets closed in current month
193 * - onlineAgents: Agents with heartbeat within 5 minutes
194 * - invoicedThisMonth: Total invoice amount for current month (excluding void)
195 * - paymentsThisMonth: Total payments received in current month
196 * - recentIncidents: Last 5 alert/warning notifications with formatted time
198 * **Time Calculations:**
199 * - "This month": DATE_TRUNC('month', CURRENT_DATE) comparison
200 * - "Online": last_seen > NOW() - INTERVAL '5 minutes'
201 * - Time ago formatting: Minutes (m), hours (h), or days (d)
203 * **Financial Metrics:**
204 * - invoicedThisMonth: Sum of issued_date within current month, status != 'void'
205 * - paymentsThisMonth: Sum of invoices with status='paid' updated this month
206 * - Note: Payment tracking approximation (updated_at used as proxy for payment date)
208 * **Incident Formatting:**
209 * Recent incidents include:
210 * - id: notification_id
211 * - message: Notification title
212 * - type: alert or warning
213 * - time: Human-readable relative time ("15m ago", "3h ago", "2d ago")
216 * Uses COUNT(*)::int and SUM()::numeric casts for type consistency. All queries use
217 * tenant filter for security. Parallel execution possible via multiple connections.
218 * @apiHeader {string} Authorization Bearer JWT token
219 * @apiSuccess {number} activeTickets Count of open and in-progress tickets
220 * @apiSuccess {number} criticalAlerts Count of unread critical alert notifications
221 * @apiSuccess {number} ticketsClosedThisMonth Tickets closed in current calendar month
222 * @apiSuccess {number} onlineAgents Count of agents with heartbeat within 5 minutes
223 * @apiSuccess {number} invoicedThisMonth Total invoice amount for current month (excluding void)
224 * @apiSuccess {number} paymentsThisMonth Total payments received in current month
225 * @apiSuccess {object[]} recentIncidents Last 5 alert/warning incidents
226 * @apiSuccess {number} recentIncidents.id Notification ID
227 * @apiSuccess {string} recentIncidents.message Incident message/title
228 * @apiSuccess {string} recentIncidents.type Incident type (alert, warning)
229 * @apiSuccess {string} recentIncidents.time Human-readable time ago ("15m ago", "3h ago")
230 * @apiError {number} 500 Failed to fetch dashboard stats
231 * @apiExample {curl} Example Request:
233 * -H "Authorization: Bearer eyJhbGc..." \
234 * "https://api.everydaytech.au/dashboard/stats"
235 * @apiExample {json} Success Response:
238 * "activeTickets": 23,
239 * "criticalAlerts": 5,
240 * "ticketsClosedThisMonth": 147,
241 * "onlineAgents": 432,
242 * "invoicedThisMonth": 147250.50,
243 * "paymentsThisMonth": 132440.00,
244 * "recentIncidents": [
247 * "message": "Server CPU above 90% for 15 minutes",
253 * "message": "Backup failed on ACME-SERVER-01",
259 * "message": "Disk space critical: 95% used",
266 * @see {@link module:routes/dashboard.getAgentMetrics} for agent health metrics
267 * @see {@link module:routes/tickets} for ticketing system
268 * @see {@link module:routes/invoices} for billing
270router.get('/stats', async (req, res) => {
272 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req);
273 const whereClause = tenantClause ? `WHERE ${tenantClause}` : '';
275 // Get active tickets count
276 const activeTicketsResult = await pool.query(
277 `SELECT COUNT(*)::int AS count FROM tickets ${whereClause ? whereClause + ' AND' : 'WHERE'} status IN ('open', 'in_progress')`,
280 const activeTickets = activeTicketsResult.rows[0]?.count || 0;
282 // Get critical alerts count
283 const criticalAlertsResult = await pool.query(
284 `SELECT COUNT(*)::int AS count FROM notifications ${whereClause ? whereClause + ' AND' : 'WHERE'} type = 'alert' AND is_read = false`,
287 const criticalAlerts = criticalAlertsResult.rows[0]?.count || 0;
289 // Get tickets closed this month
290 const ticketsClosedThisMonthResult = await pool.query(
291 `SELECT COUNT(*)::int AS count FROM tickets
292 ${whereClause ? whereClause + ' AND' : 'WHERE'} status = 'closed'
293 AND DATE_TRUNC('month', updated_at) = DATE_TRUNC('month', CURRENT_DATE)`,
296 const ticketsClosedThisMonth = ticketsClosedThisMonthResult.rows[0]?.count || 0;
298 // Get online agents count
299 const onlineAgentsResult = await pool.query(
300 `SELECT COUNT(*)::int AS count FROM agents ${whereClause ? whereClause + ' AND' : 'WHERE'} last_seen > NOW() - INTERVAL '5 minutes'`,
303 const onlineAgents = onlineAgentsResult.rows[0]?.count || 0;
305 // Get invoices this month
306 const invoicedThisMonthResult = await pool.query(
307 `SELECT COALESCE(SUM(amount), 0)::numeric AS total FROM invoices
308 ${whereClause ? whereClause + ' AND' : 'WHERE'} DATE_TRUNC('month', issued_date) = DATE_TRUNC('month', CURRENT_DATE)
309 AND status != 'void'`,
312 const invoicedThisMonth = parseFloat(invoicedThisMonthResult.rows[0]?.total || 0);
314 // Get payments received this month (approximation)
315 const paymentsThisMonthResult = await pool.query(
316 `SELECT COALESCE(SUM(amount), 0)::numeric AS total FROM invoices
317 ${whereClause ? whereClause + ' AND' : 'WHERE'} status = 'paid'
318 AND DATE_TRUNC('month', updated_at) = DATE_TRUNC('month', CURRENT_DATE)`,
321 const paymentsThisMonth = parseFloat(paymentsThisMonthResult.rows[0]?.total || 0);
323 // Get recent incidents
324 const recentIncidentsResult = await pool.query(
325 `SELECT notification_id as id, title as message, type, created_at
327 ${whereClause ? whereClause + ' AND' : 'WHERE'} type IN ('alert', 'warning')
328 ORDER BY created_at DESC
333 const recentIncidents = recentIncidentsResult.rows.map(incident => ({
335 message: incident.message,
337 time: formatTimeAgo(incident.created_at)
343 ticketsClosedThisMonth,
350 console.error('Error fetching dashboard stats:', err);
351 res.status(500).json({ error: 'Failed to fetch dashboard stats' });
355// Helper function to format time ago
360function formatTimeAgo(date) {
361 const now = new Date();
362 const diffMs = now - new Date(date);
363 const diffMins = Math.floor(diffMs / 60000);
364 const diffHours = Math.floor(diffMs / 3600000);
365 const diffDays = Math.floor(diffMs / 86400000);
367 if (diffMins < 60) return `${diffMins}m ago`;
368 if (diffHours < 24) return `${diffHours}h ago`;
369 return `${diffDays}d ago`;
372module.exports = router;