feat: create dashboard of updates

This commit is contained in:
Malena Aguiar committed 2026-09-10 18:00:12 -06:00
1 parent 8bb60bfd03
commit 896d73f12e
28 files changed
+1465 -3

No files matched your search

@@ -0,0 +1,28 @@
import { Request, Response, RequestHandler } from "express";
import { catchAsync } from "@/utils/catchAsync.js";
import { requireTeamId } from "@/api/controllers/controllerUtils.js";
import type { IDashboardService } from "@/domain/dashboard/dashboard.service.js";
export interface IDashboardController {
getSummary: RequestHandler;
}
class DashboardController implements IDashboardController {
private dashboardService: IDashboardService;
constructor(dashboardService: IDashboardService) {
this.dashboardService = dashboardService;
}
getSummary = catchAsync(async (req: Request, res: Response) => {
const teamId = requireTeamId(req.user?.teamId);
const data = await this.dashboardService.getSummary(teamId);
return res.status(200).json({
success: true,
msg: "Dashboard summary retrieved successfully",
data,
});
});
}
export default DashboardController;
+8
View File
@@ -0,0 +1,8 @@
import { Router } from "express";
import type { IDashboardController } from "@/api/controllers/dashboardController.js";
export const createDashboardRoutes = (dashboardController: IDashboardController): Router => {
const router = Router();
router.get("/summary", dashboardController.getSummary);
return router;
};
+4
View File
@@ -13,6 +13,8 @@ import TagsController from "../api/controllers/tagController.js";
import DiagnosticController from "../api/controllers/diagnosticController.js";
import IncidentController from "../api/controllers/incidentController.js";
import ProxyController from "@/api/controllers/proxyController.js";
import DashboardController from "@/api/controllers/dashboardController.js";
import type { IDashboardController } from "@/api/controllers/dashboardController.js";
import { ApiServices } from "@/config/services.api.js";
export interface InitializedControllers {
@@ -31,6 +33,7 @@ export interface InitializedControllers {
diagnosticController: DiagnosticController;
incidentController: IncidentController;
proxyController: ProxyController;
dashboardController: IDashboardController;
}
export const initializeControllers = (apiServices: ApiServices): InitializedControllers => {
return {
@@ -49,5 +52,6 @@ export const initializeControllers = (apiServices: ApiServices): InitializedCont
diagnosticController: new DiagnosticController(apiServices.diagnosticService),
incidentController: new IncidentController(apiServices.incidentService),
proxyController: new ProxyController(apiServices.proxiesService),
dashboardController: new DashboardController(apiServices.dashboardService),
};
};
+3
View File
@@ -20,6 +20,7 @@ import { createNotificationRoutes } from "@/api/routes/notificationRoutes.js";
import { createTagRoutes } from "@/api/routes/tagRoutes.js";
import { createIncidentRoutes } from "@/api/routes/incidentRoutes.js";
import { createProxyRoutes } from "@/api/routes/proxyRoutes.js";
import { createDashboardRoutes } from "@/api/routes/dashboardRoutes.js";
export const setupRoutes = (app: Application, controllers: InitializedControllers, apiServices: ApiServices) => {
const verifyJWT = createVerifyJWT(apiServices.settingsService);
@@ -39,6 +40,7 @@ export const setupRoutes = (app: Application, controllers: InitializedController
const diagnosticRoutes = createDiagnosticRoutes(controllers.diagnosticController, verifyJWT);
const incidentRoutes = createIncidentRoutes(controllers.incidentController);
const proxyRoutes = createProxyRoutes(controllers.proxyController);
const dashboardRoutes = createDashboardRoutes(controllers.dashboardController);
app.use("/api/v1/auth", authApiLimiter, authRoutes);
app.use("/api/v1/monitors", verifyJWT, monitorRoutes);
@@ -55,4 +57,5 @@ export const setupRoutes = (app: Application, controllers: InitializedController
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes);
app.use("/api/v1/incidents", verifyJWT, incidentRoutes);
app.use("/api/v1/proxies", verifyJWT, proxyRoutes);
app.use("/api/v1/dashboard", verifyJWT, dashboardRoutes);
};
+4
View File
@@ -7,6 +7,7 @@ import { ITagsService, TagsService } from "@/domain/tags/tag.service.js";
import { IUserService, UserService } from "@/domain/users/user.service.js";
import { IJobScheduler } from "@/worker/worker.interface.js";
import { ProxiesService, IProxiesService } from "@/domain/proxies/proxy.service.js";
import { DashboardService, IDashboardService } from "@/domain/dashboard/dashboard.service.js";
import { SharedServices } from "@/config/services.shared.js";
// Third-party
@@ -24,6 +25,7 @@ export interface ApiServices extends SharedServices {
tagsService: ITagsService;
diagnosticService: IDiagnosticService;
proxiesService: IProxiesService;
dashboardService: IDashboardService;
}
export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): ApiServices => {
@@ -99,6 +101,7 @@ export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): A
const tagsService = new TagsService(tagsRepository, monitorsRepository);
const diagnosticService = new DiagnosticService(db);
const proxiesService = new ProxiesService(proxiesRepository, monitorsRepository, settingsService);
const dashboardService = new DashboardService(monitorsRepository);
return {
...shared,
worker: jobScheduler,
@@ -110,5 +113,6 @@ export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): A
tagsService,
diagnosticService,
proxiesService,
dashboardService,
};
};
@@ -0,0 +1,72 @@
import type { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
import type { Monitor } from "@/domain/monitors/monitor.type.js";
import type { DashboardMonitorRow, DashboardSummaryResult, DashboardTypeCount, DashboardGroupCount } from "@/domain/dashboard/dashboard.type.js";
const TOP_N = 5;
const avgResponseTime = (monitor: Monitor): number => {
const times = (monitor.recentChecks ?? []).map((c) => c.responseTime).filter((t): t is number => t != null && t > 0);
return times.length ? times.reduce((a, b) => a + b, 0) / times.length : 0;
};
const toRow = (monitor: Monitor): DashboardMonitorRow => ({
id: monitor.id,
name: monitor.name,
status: monitor.status,
type: monitor.type,
group: monitor.group ?? null,
uptimePercentage: (monitor.uptimePercentage ?? 1) * 100,
avgResponseTime: avgResponseTime(monitor),
});
export interface IDashboardService {
getSummary(teamId: string): Promise<DashboardSummaryResult>;
}
export class DashboardService implements IDashboardService {
private monitorsRepository: IMonitorsRepository;
constructor(monitorsRepository: IMonitorsRepository) {
this.monitorsRepository = monitorsRepository;
}
getSummary = async (teamId: string): Promise<DashboardSummaryResult> => {
const [summary, monitors] = await Promise.all([
this.monitorsRepository.findMonitorsSummaryByTeamId(teamId),
this.monitorsRepository.findByTeamIdWithStats(teamId, { limit: 200 }),
]);
const rows = monitors.map(toRow);
const slowestMonitors = [...rows]
.filter((r) => r.avgResponseTime > 0)
.sort((a, b) => b.avgResponseTime - a.avgResponseTime)
.slice(0, TOP_N);
const lowestUptimeMonitors = [...rows].sort((a, b) => a.uptimePercentage - b.uptimePercentage).slice(0, TOP_N);
const downMonitors = rows.filter((r) => r.status === "down");
const typeMap = new Map<string, number>();
rows.forEach((r) => typeMap.set(r.type, (typeMap.get(r.type) ?? 0) + 1));
const monitorsByType: DashboardTypeCount[] = [...typeMap.entries()].map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count);
const groupMap = new Map<string, number>();
rows.forEach((r) => {
if (r.group) groupMap.set(r.group, (groupMap.get(r.group) ?? 0) + 1);
});
const monitorsByGroup: DashboardGroupCount[] = [...groupMap.entries()]
.map(([group, count]) => ({ group, count }))
.sort((a, b) => b.count - a.count);
return {
summary,
monitors: rows,
slowestMonitors,
lowestUptimeMonitors,
downMonitors,
monitorsByType,
monitorsByGroup,
};
};
}
@@ -0,0 +1,31 @@
import type { MonitorsSummary } from "@/domain/monitors/monitor.type.js";
export interface DashboardMonitorRow {
id: string;
name: string;
status: string;
type: string;
group: string | null;
uptimePercentage: number;
avgResponseTime: number;
}
export interface DashboardTypeCount {
type: string;
count: number;
}
export interface DashboardGroupCount {
group: string;
count: number;
}
export interface DashboardSummaryResult {
summary: MonitorsSummary;
monitors: DashboardMonitorRow[];
slowestMonitors: DashboardMonitorRow[];
lowestUptimeMonitors: DashboardMonitorRow[];
downMonitors: DashboardMonitorRow[];
monitorsByType: DashboardTypeCount[];
monitorsByGroup: DashboardGroupCount[];
}