feat: add Job Monitor page with cron job logging and Selenium queue status

This commit is contained in:
ff
2026-04-13 00:32:18 -04:00
parent 034c0fa993
commit 11a6d2e5a7
85 changed files with 3046 additions and 12 deletions

View File

@@ -3,6 +3,7 @@ import fs from "fs";
import path from "path";
import { storage } from "../storage";
import { backupDatabaseToPath } from "../services/databaseBackupService";
import { cronJobLogStorage } from "../storage/cron-job-log-storage";
// Local backup folder in the app root (apps/Backend/backups)
const LOCAL_BACKUP_DIR = path.resolve(process.cwd(), "backups");
@@ -45,17 +46,26 @@ export const startBackupCron = () => {
if (!admin.autoBackupEnabled) {
console.log("✅ [8 PM] Auto-backup is disabled for admin, skipped.");
const startedAt = new Date();
const log = await cronJobLogStorage.createJobLog("local-backup", startedAt);
await cronJobLogStorage.completeJobLog(log.id, "skipped", new Date());
return;
}
const startedAt = new Date();
const log = await cronJobLogStorage.createJobLog("local-backup", startedAt);
try {
const filename = `dental_backup_${Date.now()}.sql`;
await backupDatabaseToPath({ destinationPath: LOCAL_BACKUP_DIR, filename });
await storage.createBackup(admin.id);
await storage.deleteNotificationsByType(admin.id, "BACKUP");
await cronJobLogStorage.completeJobLog(log.id, "success", new Date());
console.log(`✅ Local backup done → ${filename}`);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error("Local backup failed:", err);
await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage);
await storage.createNotification(
admin.id,
"BACKUP",
@@ -80,11 +90,19 @@ export const startBackupCron = () => {
if (!admin.usbBackupEnabled) {
console.log("✅ [9 PM] USB backup is disabled for admin, skipped.");
const startedAt = new Date();
const log = await cronJobLogStorage.createJobLog("usb-backup", startedAt);
await cronJobLogStorage.completeJobLog(log.id, "skipped", new Date());
return;
}
const startedAt = new Date();
const log = await cronJobLogStorage.createJobLog("usb-backup", startedAt);
const destination = await storage.getActiveBackupDestination(admin.id);
if (!destination) {
const errorMessage = "No backup destination configured.";
await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage);
await storage.createNotification(
admin.id,
"BACKUP",
@@ -96,6 +114,8 @@ export const startBackupCron = () => {
const usbBackupPath = path.join(destination.path, USB_BACKUP_FOLDER_NAME);
if (!fs.existsSync(usbBackupPath)) {
const errorMessage = `Folder "${USB_BACKUP_FOLDER_NAME}" not found on the drive.`;
await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage);
await storage.createNotification(
admin.id,
"BACKUP",
@@ -109,9 +129,12 @@ export const startBackupCron = () => {
await backupDatabaseToPath({ destinationPath: usbBackupPath, filename });
await storage.createBackup(admin.id);
await storage.deleteNotificationsByType(admin.id, "BACKUP");
await cronJobLogStorage.completeJobLog(log.id, "success", new Date());
console.log(`✅ USB backup done → ${usbBackupPath}/${filename}`);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error("USB backup failed:", err);
await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage);
await storage.createNotification(
admin.id,
"BACKUP",

View File

@@ -19,6 +19,7 @@ import paymentOcrRoutes from "./paymentOcrExtraction";
import cloudStorageRoutes from "./cloud-storage";
import paymentsReportsRoutes from "./payments-reports";
import exportPaymentsReportsRoutes from "./export-payments-reports";
import jobMonitorRoutes from "./job-monitor";
const router = Router();
@@ -42,5 +43,6 @@ router.use("/payment-ocr", paymentOcrRoutes);
router.use("/cloud-storage", cloudStorageRoutes);
router.use("/payments-reports", paymentsReportsRoutes);
router.use("/export-payments-reports", exportPaymentsReportsRoutes);
router.use("/job-monitor", jobMonitorRoutes);
export default router;

View File

@@ -0,0 +1,51 @@
import { Router, Request, Response } from "express";
import axios from "axios";
import { cronJobLogStorage } from "../storage/cron-job-log-storage";
const router = Router();
const SELENIUM_BASE_URL =
process.env.SELENIUM_AGENT_BASE_URL || "http://localhost:5002";
// GET /api/job-monitor/summary
// Returns last run per cron job + recent history
router.get("/summary", async (_req: Request, res: Response) => {
try {
const [lastRuns, recentLogs] = await Promise.all([
cronJobLogStorage.getLastRunPerJob(),
cronJobLogStorage.getRecentLogs(30),
]);
res.json({ lastRuns, recentLogs });
} catch (err) {
console.error("job-monitor/summary error:", err);
res.status(500).json({ error: "Failed to fetch cron job summary" });
}
});
// GET /api/job-monitor/failed
// Returns recent failed job logs
router.get("/failed", async (_req: Request, res: Response) => {
try {
const failed = await cronJobLogStorage.getFailedLogs(20);
res.json(failed);
} catch (err) {
console.error("job-monitor/failed error:", err);
res.status(500).json({ error: "Failed to fetch failed jobs" });
}
});
// GET /api/job-monitor/selenium-status
// Proxies the Selenium service /status endpoint
router.get("/selenium-status", async (_req: Request, res: Response) => {
try {
const response = await axios.get(`${SELENIUM_BASE_URL}/status`, {
timeout: 4000,
});
res.json({ online: true, ...response.data });
} catch (err) {
// Service may be offline — return gracefully
res.json({ online: false, active_jobs: 0, queued_jobs: 0, status: "offline" });
}
});
export default router;

View File

@@ -0,0 +1,61 @@
import { prisma as db } from "@repo/db/client";
export type CronJobStatus = "success" | "failed" | "skipped";
export interface CronJobLogEntry {
id: number;
jobName: string;
status: string;
startedAt: Date;
completedAt: Date | null;
durationMs: number | null;
errorMessage: string | null;
}
export const cronJobLogStorage = {
async createJobLog(
jobName: string,
startedAt: Date
): Promise<CronJobLogEntry> {
return db.cronJobLog.create({
data: { jobName, status: "running", startedAt },
});
},
async completeJobLog(
id: number,
status: CronJobStatus,
completedAt: Date,
errorMessage?: string
): Promise<CronJobLogEntry> {
const durationMs = completedAt.getTime() - (await db.cronJobLog.findUniqueOrThrow({ where: { id } })).startedAt.getTime();
return db.cronJobLog.update({
where: { id },
data: { status, completedAt, durationMs, errorMessage: errorMessage ?? null },
});
},
async getRecentLogs(limit = 50): Promise<CronJobLogEntry[]> {
return db.cronJobLog.findMany({
orderBy: { startedAt: "desc" },
take: limit,
});
},
async getLastRunPerJob(): Promise<CronJobLogEntry[]> {
// Get the most recent log entry for each distinct jobName
const jobs = await db.cronJobLog.findMany({
distinct: ["jobName"],
orderBy: { startedAt: "desc" },
});
return jobs;
},
async getFailedLogs(limit = 20): Promise<CronJobLogEntry[]> {
return db.cronJobLog.findMany({
where: { status: "failed" },
orderBy: { startedAt: "desc" },
take: limit,
});
},
};

View File

@@ -16,6 +16,7 @@ import { cloudStorageStorage } from './cloudStorage-storage';
import { paymentsReportsStorage } from './payments-reports-storage';
import { patientDocumentsStorage } from './patientDocuments-storage';
import * as exportPaymentsReportsStorage from "./export-payments-reports-storage";
import { cronJobLogStorage } from "./cron-job-log-storage";
export const storage = {
@@ -34,7 +35,8 @@ export const storage = {
...cloudStorageStorage,
...paymentsReportsStorage,
...patientDocumentsStorage,
...exportPaymentsReportsStorage,
...exportPaymentsReportsStorage,
...cronJobLogStorage,
};