EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
dashboard.js
Go to the documentation of this file.
1/**
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.
7 *
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
15 *
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.
19 *
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
25 *
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.
29 *
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)
35 *
36 * **Use Cases:**
37 * - Homepage dashboard overview
38 * - Real-time monitoring dashboards
39 * - Executive reporting
40 * - Health check status boards
41 * - MSP portal home page
42 * @requires express
43 * @requires ../services/db
44 * @requires ../middleware/auth
45 * @requires ../middleware/tenant
46 * @author IBG MSP Development Team
47 * @date 2024-03-11
48 * @since 1.0.0
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
52 */
53
54const express = require('express');
55const router = express.Router();
56const pool = require('../services/db');
57const authenticateToken = require('../middleware/auth');
58const { getTenantFilter, setTenantContext } = require('../middleware/tenant');
59
60// Apply authentication and tenant context to all routes
61router.use(authenticateToken, setTenantContext);
62
63
64
65/**
66 * @api {get} /dashboard/agent-metrics Get Agent Metrics
67 * @apiName GetAgentMetrics
68 * @apiGroup Dashboard
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.
72 *
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
79 *
80 * **Data Freshness:**
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).
83 *
84 * **Agent States:**
85 * - metrics: non-null - Agent reporting metrics
86 * - metrics: null - Agent never reported metrics or data purged
87 *
88 * **Tenant Filtering:**
89 * Only returns agents belonging to authenticated user's tenant. Root MSP users see all
90 * tenants' agents.
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:
108 * curl -X GET \
109 * -H "Authorization: Bearer eyJhbGc..." \
110 * "https://api.everydaytech.au/dashboard/agent-metrics"
111 * @apiExample {json} Success Response:
112 * HTTP/1.1 200 OK
113 * {
114 * "agents": [
115 * {
116 * "agent_id": 142,
117 * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000",
118 * "hostname": "ACME-DESKTOP-01",
119 * "os": "Windows 11 Pro",
120 * "customer_id": 45,
121 * "metrics": {
122 * "agent_id": 142,
123 * "ts": "2024-03-11T17:58:00.000Z",
124 * "cpu": 24.5,
125 * "memory": 62.3,
126 * "disk": 78.1,
127 * "uptime": 432000,
128 * "raw": {"cpu_cores": 8, "memory_total_gb": 32, "disk_total_gb": 512}
129 * }
130 * },
131 * {
132 * "agent_id": 89,
133 * "agent_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
134 * "hostname": "ACME-SERVER-01",
135 * "os": "Ubuntu 22.04 LTS",
136 * "customer_id": 45,
137 * "metrics": null
138 * }
139 * ]
140 * }
141 *
142 * @since 1.0.0
143 * @see {@link module:routes/agent} for agent management
144 * @see {@link module:routes/dashboard.getStats} for dashboard statistics
145 */
146router.get('/agent-metrics', async (req, res) => {
147 try {
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}`,
153 tenantParams
154 );
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
159 FROM agent_metrics
160 WHERE agent_id IN (${agents.map(a => a.agent_id).join(',') || 'NULL'})
161 ORDER BY agent_id, ts DESC`
162 );
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,
170 os: agent.os,
171 customer_id: agent.customer_id,
172 metrics: metricsByAgent[agent.agent_id] || null
173 }));
174 res.json({ agents: result });
175 } catch (err) {
176 console.error('Error fetching agent metrics:', err);
177 res.status(500).json({ error: 'Failed to fetch agent metrics' });
178 }
179});
180
181/**
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.
188 *
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
197 *
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)
202 *
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)
207 *
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")
214 *
215 * **Performance:**
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:
232 * curl -X GET \
233 * -H "Authorization: Bearer eyJhbGc..." \
234 * "https://api.everydaytech.au/dashboard/stats"
235 * @apiExample {json} Success Response:
236 * HTTP/1.1 200 OK
237 * {
238 * "activeTickets": 23,
239 * "criticalAlerts": 5,
240 * "ticketsClosedThisMonth": 147,
241 * "onlineAgents": 432,
242 * "invoicedThisMonth": 147250.50,
243 * "paymentsThisMonth": 132440.00,
244 * "recentIncidents": [
245 * {
246 * "id": 891,
247 * "message": "Server CPU above 90% for 15 minutes",
248 * "type": "alert",
249 * "time": "15m ago"
250 * },
251 * {
252 * "id": 884,
253 * "message": "Backup failed on ACME-SERVER-01",
254 * "type": "warning",
255 * "time": "3h ago"
256 * },
257 * {
258 * "id": 875,
259 * "message": "Disk space critical: 95% used",
260 * "type": "alert",
261 * "time": "1d ago"
262 * }
263 * ]
264 * }
265 * @since 1.0.0
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
269 */
270router.get('/stats', async (req, res) => {
271 try {
272 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req);
273 const whereClause = tenantClause ? `WHERE ${tenantClause}` : '';
274
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')`,
278 tenantParams
279 );
280 const activeTickets = activeTicketsResult.rows[0]?.count || 0;
281
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`,
285 tenantParams
286 );
287 const criticalAlerts = criticalAlertsResult.rows[0]?.count || 0;
288
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)`,
294 tenantParams
295 );
296 const ticketsClosedThisMonth = ticketsClosedThisMonthResult.rows[0]?.count || 0;
297
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'`,
301 tenantParams
302 );
303 const onlineAgents = onlineAgentsResult.rows[0]?.count || 0;
304
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'`,
310 tenantParams
311 );
312 const invoicedThisMonth = parseFloat(invoicedThisMonthResult.rows[0]?.total || 0);
313
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)`,
319 tenantParams
320 );
321 const paymentsThisMonth = parseFloat(paymentsThisMonthResult.rows[0]?.total || 0);
322
323 // Get recent incidents
324 const recentIncidentsResult = await pool.query(
325 `SELECT notification_id as id, title as message, type, created_at
326 FROM notifications
327 ${whereClause ? whereClause + ' AND' : 'WHERE'} type IN ('alert', 'warning')
328 ORDER BY created_at DESC
329 LIMIT 5`,
330 tenantParams
331 );
332
333 const recentIncidents = recentIncidentsResult.rows.map(incident => ({
334 id: incident.id,
335 message: incident.message,
336 type: incident.type,
337 time: formatTimeAgo(incident.created_at)
338 }));
339
340 res.json({
341 activeTickets,
342 criticalAlerts,
343 ticketsClosedThisMonth,
344 onlineAgents,
345 invoicedThisMonth,
346 paymentsThisMonth,
347 recentIncidents
348 });
349 } catch (err) {
350 console.error('Error fetching dashboard stats:', err);
351 res.status(500).json({ error: 'Failed to fetch dashboard stats' });
352 }
353});
354
355// Helper function to format time ago
356/**
357 *
358 * @param date
359 */
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);
366
367 if (diffMins < 60) return `${diffMins}m ago`;
368 if (diffHours < 24) return `${diffHours}h ago`;
369 return `${diffDays}d ago`;
370}
371
372module.exports = router;