From f648ab1b243a11e2bff9e9276c5249c088f75e40 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 4 May 2026 11:52:45 -0700 Subject: [PATCH 1/5] add geoInterval, monitorActive, and repeat to jobs data --- .../SuperSimpleQueue/SuperSimpleQueue.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 1d50d720c..58e7a9585 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -28,6 +28,8 @@ type QueueJobSummary = { monitorUrl: string | null; monitorType: string | null; monitorInterval: number | null; + monitorGeoInterval: number | null; + monitorActive: boolean | null; active: boolean; lockedAt: number | null; runCount: number; @@ -37,6 +39,7 @@ type QueueJobSummary = { lastFinishedAt: number | null; lastRunTook: number | null; lastFailedAt: number | null; + repeat: number | null; }; export interface ISuperSimpleQueue { @@ -223,6 +226,8 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { if (result === false) { throw new Error("Failed to pause monitor"); } + this.scheduler.updateJob(monitor.id, { data: monitor }); + const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`); if (geoJob) { const geoRes = await this.scheduler.pauseJob(`${monitor.id}-geo`); @@ -233,6 +238,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { method: "pauseJob", }); } + this.scheduler.updateJob(`${monitor.id}-geo`, { data: monitor }); } this.logger.debug({ @@ -247,6 +253,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { if (result === false) { throw new Error("Failed to resume monitor"); } + this.scheduler.updateJob(monitor.id, { data: monitor }); const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`); if (geoJob) { @@ -258,6 +265,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { method: "resumeJob", }); } + this.scheduler.updateJob(`${monitor.id}-geo`, { data: monitor }); } this.logger.debug({ message: `Resumed monitor ${monitor.id}`, @@ -359,7 +367,10 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { monitorUrl: job.data?.url || null, monitorType: job.data?.type || null, monitorInterval: job.data?.interval || null, + monitorGeoInterval: job.data?.geoCheckInterval || null, + monitorActive: job.data?.isActive ?? null, active: job.active, + repeat: job.repeat ?? null, lockedAt: job.lockedAt ?? null, runCount: job.runCount ?? 0, failCount: job.failCount ?? 0, From 22230b8a08f4882eb91b88cc2fb3734b54b47df4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 4 May 2026 11:53:12 -0700 Subject: [PATCH 2/5] UI fixes for job queue table --- client/src/Pages/Logs/TabQueue.tsx | 2 +- .../src/Pages/Logs/components/TableJobs.tsx | 47 ++++++++++++++++--- client/src/Types/Queue.ts | 3 ++ client/src/locales/en.json | 5 +- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx index 84573e8f0..51c9693c0 100644 --- a/client/src/Pages/Logs/TabQueue.tsx +++ b/client/src/Pages/Logs/TabQueue.tsx @@ -19,7 +19,7 @@ export const TabQueue = () => { isLoading, // error, refetch, - } = useGet("/queue/all-metrics", {}, { refreshInterval: 5000 }); + } = useGet("/queue/all-metrics", {}, { refreshInterval: 1000 }); const { post, loading: isFlushing } = usePost(); diff --git a/client/src/Pages/Logs/components/TableJobs.tsx b/client/src/Pages/Logs/components/TableJobs.tsx index f690bd417..1f1da1003 100644 --- a/client/src/Pages/Logs/components/TableJobs.tsx +++ b/client/src/Pages/Logs/components/TableJobs.tsx @@ -30,6 +30,22 @@ export const TableJobs = ({ jobs }: TableJobsProps) => { }; const headers: Header[] = [ + { + id: "state", + content: t("pages.logs.table.headers.state"), + render: (row) => { + if (row.lockedAt) { + return t("common.labels.running"); + } + if (row.monitorActive === true) { + return t("common.labels.active"); + } + if (row.monitorActive === false) { + return t("common.labels.paused"); + } + return t("common.labels.idle"); + }, + }, { id: "id", content: t("common.table.headers.monitorId"), @@ -47,7 +63,10 @@ export const TableJobs = ({ jobs }: TableJobsProps) => { id: "url", content: t("common.table.headers.url"), render: (row) => ( - + {row.monitorUrl} ), @@ -60,9 +79,15 @@ export const TableJobs = ({ jobs }: TableJobsProps) => { { id: "interval", content: t("common.table.headers.interval"), - render: (row) => ( - {prettyMilliseconds(row.monitorInterval ?? 0)} - ), + render: (row) => { + let interval; + if (row.monitorId.toString().includes("geo")) { + interval = row.monitorGeoInterval ?? 0; + } else { + interval = row.repeat ?? 0; + } + return {prettyMilliseconds(interval)}; + }, }, { id: "lastRun", @@ -70,7 +95,10 @@ export const TableJobs = ({ jobs }: TableJobsProps) => { render: (row) => { const v = formatTimestamp(row.lastRunAt) ?? "-"; return ( - + {v} ); @@ -90,7 +118,10 @@ export const TableJobs = ({ jobs }: TableJobsProps) => { render: (row) => { const v = formatTimestamp(row.lockedAt) ?? "-"; return ( - + {v} ); @@ -100,6 +131,7 @@ export const TableJobs = ({ jobs }: TableJobsProps) => { const isDark = theme.palette.mode === "dark"; const runningBg = isDark ? "rgba(19, 113, 91, 0.18)" : "#ECF7F2"; + const pausedBg = isDark ? "rgba(255, 165, 0, 0.18)" : "#FFF4E5"; return ( { ...(row.lockedAt && { "& td": { backgroundColor: runningBg }, }), + ...(row.monitorActive === false && { + "& td": { backgroundColor: pausedBg }, + }), })} /> ); diff --git a/client/src/Types/Queue.ts b/client/src/Types/Queue.ts index 77a770aeb..bbb3ca9a4 100644 --- a/client/src/Types/Queue.ts +++ b/client/src/Types/Queue.ts @@ -23,6 +23,8 @@ export interface QueueJobSummary { monitorUrl: string | null; monitorType: MonitorType | null; monitorInterval: number | null; + monitorGeoInterval: number | null; + monitorActive: boolean | null; active: boolean; lockedAt: number | null; runCount: number; @@ -32,6 +34,7 @@ export interface QueueJobSummary { lastFinishedAt: number | null; lastRunTook: number | null; lastFailedAt: number | null; + repeat: number | null; } export interface QueueData { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index b5fe0c1cb..a08cdfe8a 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -70,7 +70,9 @@ }, "labels": { "active": "Active", - "paused": "paused", + "running": "Running", + "paused": "Paused", + "idle": "Idle", "na": "N/A", "resolved": "Resolved", "responseTime": "Response time" @@ -887,6 +889,7 @@ }, "table": { "headers": { + "state": "State", "timestamp": "Timestamp", "level": "Level", "service": "Service", From 1fd56610eaca3e1b55f95e4f013a93551de56b5c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 4 May 2026 11:55:39 -0700 Subject: [PATCH 3/5] update tests --- .../unit/services/superSimpleQueue.test.ts | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/server/test/unit/services/superSimpleQueue.test.ts b/server/test/unit/services/superSimpleQueue.test.ts index 95367cb5a..685dd71c7 100644 --- a/server/test/unit/services/superSimpleQueue.test.ts +++ b/server/test/unit/services/superSimpleQueue.test.ts @@ -302,6 +302,17 @@ describe("SuperSimpleQueue", () => { expect(logger.debug).toHaveBeenCalledWith(expect.objectContaining({ message: "Paused monitor mon-1" })); }); + it("refreshes scheduler data with current monitor on pause", async () => { + const { queue, scheduler } = createQueue(); + (scheduler.getJob as jest.Mock).mockResolvedValue({ id: "mon-1-geo" }); + const monitor = makeMonitor({ isActive: false, status: "paused" }); + + await queue.pauseJob(monitor); + + expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1", { data: monitor }); + expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1-geo", { data: monitor }); + }); + it("does not pause geo job when it does not exist", async () => { const { queue, scheduler } = createQueue(); await queue.pauseJob(makeMonitor()); @@ -309,12 +320,26 @@ describe("SuperSimpleQueue", () => { expect(scheduler.pauseJob).not.toHaveBeenCalledWith("mon-1-geo"); }); + it("does not refresh geo data when geo job does not exist", async () => { + const { queue, scheduler } = createQueue(); + await queue.pauseJob(makeMonitor()); + expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1", expect.objectContaining({ data: expect.anything() })); + expect(scheduler.updateJob).not.toHaveBeenCalledWith("mon-1-geo", expect.anything()); + }); + it("throws when scheduler.pauseJob returns false for the main job", async () => { const { queue, scheduler } = createQueue(); (scheduler.pauseJob as jest.Mock).mockResolvedValue(false); await expect(queue.pauseJob(makeMonitor())).rejects.toThrow("Failed to pause monitor"); }); + it("does not refresh data when main pauseJob throws", async () => { + const { queue, scheduler } = createQueue(); + (scheduler.pauseJob as jest.Mock).mockResolvedValue(false); + await expect(queue.pauseJob(makeMonitor())).rejects.toThrow(); + expect(scheduler.updateJob).not.toHaveBeenCalled(); + }); + it("logs error but does not throw when geo pauseJob returns false", async () => { const { queue, scheduler, logger } = createQueue(); (scheduler.getJob as jest.Mock).mockResolvedValue({ id: "mon-1-geo" }); @@ -338,6 +363,17 @@ describe("SuperSimpleQueue", () => { expect(logger.debug).toHaveBeenCalledWith(expect.objectContaining({ message: "Resumed monitor mon-1" })); }); + it("refreshes scheduler data with current monitor on resume", async () => { + const { queue, scheduler } = createQueue(); + (scheduler.getJob as jest.Mock).mockResolvedValue({ id: "mon-1-geo" }); + const monitor = makeMonitor({ isActive: true, status: "initializing" }); + + await queue.resumeJob(monitor); + + expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1", { data: monitor }); + expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1-geo", { data: monitor }); + }); + it("does not resume geo job when it does not exist", async () => { const { queue, scheduler } = createQueue(); await queue.resumeJob(makeMonitor()); @@ -345,12 +381,26 @@ describe("SuperSimpleQueue", () => { expect(scheduler.resumeJob).not.toHaveBeenCalledWith("mon-1-geo"); }); + it("does not refresh geo data when geo job does not exist", async () => { + const { queue, scheduler } = createQueue(); + await queue.resumeJob(makeMonitor()); + expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1", expect.objectContaining({ data: expect.anything() })); + expect(scheduler.updateJob).not.toHaveBeenCalledWith("mon-1-geo", expect.anything()); + }); + it("throws when scheduler.resumeJob returns false for the main job", async () => { const { queue, scheduler } = createQueue(); (scheduler.resumeJob as jest.Mock).mockResolvedValue(false); await expect(queue.resumeJob(makeMonitor())).rejects.toThrow("Failed to resume monitor"); }); + it("does not refresh data when main resumeJob throws", async () => { + const { queue, scheduler } = createQueue(); + (scheduler.resumeJob as jest.Mock).mockResolvedValue(false); + await expect(queue.resumeJob(makeMonitor())).rejects.toThrow(); + expect(scheduler.updateJob).not.toHaveBeenCalled(); + }); + it("logs error but does not throw when geo resumeJob returns false", async () => { const { queue, scheduler, logger } = createQueue(); (scheduler.getJob as jest.Mock).mockResolvedValue({ id: "mon-1-geo" }); @@ -507,6 +557,7 @@ describe("SuperSimpleQueue", () => { { id: "m1", active: true, + repeat: 60000, lockedAt: null, runCount: 5, failCount: 1, @@ -514,7 +565,7 @@ describe("SuperSimpleQueue", () => { lastRunAt: 1000, lastFinishedAt: 1100, lastFailedAt: 900, - data: { url: "http://a.com", type: "http", interval: 60000 }, + data: { url: "http://a.com", type: "http", interval: 60000, geoCheckInterval: 300000, isActive: true }, }, ]); const jobs = await queue.getJobs(); @@ -525,7 +576,10 @@ describe("SuperSimpleQueue", () => { monitorUrl: "http://a.com", monitorType: "http", monitorInterval: 60000, + monitorGeoInterval: 300000, + monitorActive: true, active: true, + repeat: 60000, lockedAt: null, runCount: 5, failCount: 1, @@ -553,6 +607,30 @@ describe("SuperSimpleQueue", () => { expect(jobs[0].monitorUrl).toBeNull(); expect(jobs[0].monitorType).toBeNull(); expect(jobs[0].monitorInterval).toBeNull(); + expect(jobs[0].monitorGeoInterval).toBeNull(); + expect(jobs[0].monitorActive).toBeNull(); + expect(jobs[0].repeat).toBeNull(); + }); + + it("exposes monitor.isActive as monitorActive in the summary", async () => { + const { queue, scheduler } = createQueue(); + (scheduler.getJobs as jest.Mock).mockResolvedValue([ + { id: "m1", active: true, data: { url: "a", type: "http", interval: 60000, isActive: true } }, + { id: "m2", active: false, data: { url: "b", type: "http", interval: 60000, isActive: false } }, + ]); + const jobs = await queue.getJobs(); + expect(jobs[0].monitorActive).toBe(true); + expect(jobs[1].monitorActive).toBe(false); + }); + + it("exposes geo check interval and repeat", async () => { + const { queue, scheduler } = createQueue(); + (scheduler.getJobs as jest.Mock).mockResolvedValue([ + { id: "m1", active: true, repeat: 120000, data: { url: "a", type: "http", interval: 60000, geoCheckInterval: 600000 } }, + ]); + const jobs = await queue.getJobs(); + expect(jobs[0].repeat).toBe(120000); + expect(jobs[0].monitorGeoInterval).toBe(600000); }); }); From 2e9b87ba32ee75406f43ea7195a5a8ff58e2d9ca Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 4 May 2026 11:59:02 -0700 Subject: [PATCH 4/5] normalize tabqueue --- client/src/Pages/Logs/TabQueue.tsx | 38 +++++++++++------------------ client/src/Utils/Theme/constants.ts | 1 + 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx index 51c9693c0..87d658635 100644 --- a/client/src/Pages/Logs/TabQueue.tsx +++ b/client/src/Pages/Logs/TabQueue.tsx @@ -10,6 +10,7 @@ import type { QueueData } from "@/Types/Queue"; import { Metrics } from "@/Pages/Logs/components/Metrics"; import { Button } from "@/Components/inputs"; import { EmptyState } from "@/Components/design-elements"; +import { SPACING } from "@/Utils/Theme/constants"; export const TabQueue = () => { const theme = useTheme(); @@ -49,32 +50,26 @@ export const TabQueue = () => { {t("pages.logs.jobQueue")} - + ), }} @@ -87,16 +82,11 @@ export const TabQueue = () => { {t("pages.logs.failedJobs")} - + {t("pages.logs.failedJobsExplainer")} diff --git a/client/src/Utils/Theme/constants.ts b/client/src/Utils/Theme/constants.ts index d718d61dc..518657183 100644 --- a/client/src/Utils/Theme/constants.ts +++ b/client/src/Utils/Theme/constants.ts @@ -1,4 +1,5 @@ export const SPACING = { + XXS: 0.25, XS: 0.5, SM: 1, MD: 1.5, From 3365a8f7da3f0d540da1ea521706f94a10360720 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 4 May 2026 12:11:57 -0700 Subject: [PATCH 5/5] extract colors to theme --- client/src/Pages/Logs/TabQueue.tsx | 6 +----- client/src/Pages/Logs/components/TableJobs.tsx | 8 ++------ client/src/Utils/Theme/Palette.ts | 13 +++++++++++++ client/src/Utils/Theme/Theme.ts | 2 ++ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx index 87d658635..a39250be0 100644 --- a/client/src/Pages/Logs/TabQueue.tsx +++ b/client/src/Pages/Logs/TabQueue.tsx @@ -61,11 +61,7 @@ export const TabQueue = () => { highlight: ( { }, ]; - const isDark = theme.palette.mode === "dark"; - const runningBg = isDark ? "rgba(19, 113, 91, 0.18)" : "#ECF7F2"; - const pausedBg = isDark ? "rgba(255, 165, 0, 0.18)" : "#FFF4E5"; - return (
({ ...(row.lockedAt && { - "& td": { backgroundColor: runningBg }, + "& td": { backgroundColor: theme.palette.rowStatus.running }, }), ...(row.monitorActive === false && { - "& td": { backgroundColor: pausedBg }, + "& td": { backgroundColor: theme.palette.rowStatus.paused }, }), })} /> diff --git a/client/src/Utils/Theme/Palette.ts b/client/src/Utils/Theme/Palette.ts index 92c08cd2f..55dc1e26e 100644 --- a/client/src/Utils/Theme/Palette.ts +++ b/client/src/Utils/Theme/Palette.ts @@ -1,3 +1,5 @@ +import { alpha } from "@mui/material/styles"; + const typographyBase = 13; export const typographyLevels = { @@ -17,6 +19,9 @@ export const colors = { gray850: "#1c1c21", brandGreen: "#13715B", brandGreenLight: "#4DAF94", + green200: "#ECF7F2", + yellow200: "#FFF4E5", + orange500: "#FFA500", }; export const lightPalette = { @@ -29,6 +34,10 @@ export const lightPalette = { sidebar: { accent: colors.brandGreen, }, + rowStatus: { + running: colors.green200, + paused: colors.yellow200, + }, }; export const darkPalette = { @@ -45,4 +54,8 @@ export const darkPalette = { sidebar: { accent: colors.brandGreenLight, }, + rowStatus: { + running: alpha(colors.brandGreen, 0.18), + paused: alpha(colors.orange500, 0.18), + }, }; diff --git a/client/src/Utils/Theme/Theme.ts b/client/src/Utils/Theme/Theme.ts index 7c712d66d..04f4c94eb 100644 --- a/client/src/Utils/Theme/Theme.ts +++ b/client/src/Utils/Theme/Theme.ts @@ -15,9 +15,11 @@ declare module "@mui/material/styles" { } interface Palette { sidebar: { accent: string }; + rowStatus: { running: string; paused: string }; } interface PaletteOptions { sidebar?: { accent: string }; + rowStatus?: { running: string; paused: string }; } }