From 5dde4602020e86a7bc8dac059c161be6568088e1 Mon Sep 17 00:00:00 2001 From: Shafikul Islam Tanbir <58387559+shafiktanbir@users.noreply.github.com> Date: Mon, 26 Oct 2026 22:12:39 +0600 Subject: [PATCH] fix(status-page): order reports by latest update in getLight (#3003) * fix(status-page): order reports by latest update in getLight (#3002) * ci: apply automated fixes * refactor(api): extract sortStatusReportsByLatestUpdate helper Share `sortStatusReportsByLatestUpdate` between `statusPage.get` and `statusPage.getLight` and add a direct unit test for the comparator. * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../api/src/router/statusPage.order.test.ts | 93 +++++++++++++++---- packages/api/src/router/statusPage.ts | 37 +++++--- 2 files changed, 98 insertions(+), 32 deletions(-) diff --git a/packages/api/src/router/statusPage.order.test.ts b/packages/api/src/router/statusPage.order.test.ts index dcf692cda..6930eca1f 100644 --- a/packages/api/src/router/statusPage.order.test.ts +++ b/packages/api/src/router/statusPage.order.test.ts @@ -13,6 +13,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { appRouter } from "../root"; import { createInnerTRPCContext } from "../trpc"; +import { sortStatusReportsByLatestUpdate } from "./statusPage"; let workspaceId: number; let pageId: number; @@ -27,20 +28,56 @@ function caller() { ); } -beforeAll(async () => { - const fixture = await createTestWorkspace(); - workspaceId = fixture.workspace.id; - const createdPage = await createPage(workspaceId); - pageId = createdPage.id; - slug = createdPage.slug; -}); +describe("sortStatusReportsByLatestUpdate", () => { + test("orders reports without updates first, then by latest update date descending, breaking ties by id", () => { + const reports = [ + { + id: 1, + statusReportUpdates: [{ date: new Date("2026-02-01T00:00:00Z") }], + }, + { + id: 2, + statusReportUpdates: [ + { date: new Date("2026-03-01T00:00:00Z") }, + { date: new Date("2026-01-01T00:00:00Z") }, + ], + }, + { + id: 3, + statusReportUpdates: [], + }, + { + id: 4, + statusReportUpdates: [], + }, + { + id: 5, + statusReportUpdates: [{ date: new Date("2026-03-01T00:00:00Z") }], + }, + ]; -afterAll(async () => { - await db.delete(statusReport).where(eq(statusReport.pageId, pageId)); - await db.delete(page).where(eq(page.workspaceId, workspaceId)); + const sortedIds = [...reports] + .sort(sortStatusReportsByLatestUpdate) + .map((report) => report.id); + + expect(sortedIds).toEqual([4, 3, 5, 2, 1]); + }); }); -describe("statusPage.get report order", () => { +describe("statusPage.get and statusPage.getLight report order", () => { + beforeAll(async () => { + const fixture = await createTestWorkspace(); + workspaceId = fixture.workspace.id; + const createdPage = await createPage(workspaceId); + pageId = createdPage.id; + slug = createdPage.slug; + }); + + afterAll(async () => { + await db.delete(statusReport).where(eq(statusReport.pageId, pageId)); + await db.delete(page).where(eq(page.workspaceId, workspaceId)); + }); + test("uses each report's newest update and puts reports without updates first", async () => { const olderActivity = await db .insert(statusReport) @@ -94,12 +131,17 @@ describe("statusPage.get report order", () => { }, ]); + const expectedOrder = [noUpdates.id, recentActivity.id, olderActivity.id]; + const result = await caller().statusPage.get({ slug }); - expect(result?.statusReports.map((report) => report.id)).toEqual([ - noUpdates.id, - recentActivity.id, - olderActivity.id, - ]); + expect(result?.statusReports.map((report) => report.id)).toEqual( + expectedOrder, + ); + + const lightResult = await caller().statusPage.getLight({ slug }); + expect(lightResult?.statusReports.map((report) => report.id)).toEqual( + expectedOrder, + ); }); test("breaks equal activity dates and reports without updates by id", async () => { @@ -160,6 +202,13 @@ describe("statusPage.get report order", () => { }, ]); + const expectedTieOrder = [ + secondNoUpdates.id, + firstNoUpdates.id, + second.id, + first.id, + ]; + const result = await caller().statusPage.get({ slug }); const ids = result?.statusReports.map((report) => report.id); expect( @@ -168,6 +217,16 @@ describe("statusPage.get report order", () => { id, ), ), - ).toEqual([secondNoUpdates.id, firstNoUpdates.id, second.id, first.id]); + ).toEqual(expectedTieOrder); + + const lightResult = await caller().statusPage.getLight({ slug }); + const lightIds = lightResult?.statusReports.map((report) => report.id); + expect( + lightIds?.filter((id) => + [first.id, second.id, firstNoUpdates.id, secondNoUpdates.id].includes( + id, + ), + ), + ).toEqual(expectedTieOrder); }); }); diff --git a/packages/api/src/router/statusPage.ts b/packages/api/src/router/statusPage.ts index 583d442d0..a3b15fa1b 100644 --- a/packages/api/src/router/statusPage.ts +++ b/packages/api/src/router/statusPage.ts @@ -87,6 +87,20 @@ const queryPasswordSchema = z.string().nullish(); const bySlugOrDomain = (slug: string) => sql`lower(${page.slug}) = ${slug} OR lower(${page.customDomain}) = ${slug}`; +export function sortStatusReportsByLatestUpdate< + T extends { id: number; statusReportUpdates: { date: Date }[] }, +>(a: T, b: T): number { + // Sort reports without updates to the beginning + if (a.statusReportUpdates.length === 0 && b.statusReportUpdates.length === 0) + return b.id - a.id; + if (a.statusReportUpdates.length === 0) return -1; + if (b.statusReportUpdates.length === 0) return 1; + return ( + b.statusReportUpdates[0].date.getTime() - + a.statusReportUpdates[0].date.getTime() || b.id - a.id + ); +} + // Status rows grouped into `timeZone` days, with the zone they ended up in. async function fetchStatusDays( monitors: Parameters[0], @@ -522,20 +536,9 @@ export const statusPageRouter = createTRPCRouter({ ? _page.customTheme : null; - const statusReports = _page.statusReports.sort((a, b) => { - // Sort reports without updates to the beginning - if ( - a.statusReportUpdates.length === 0 && - b.statusReportUpdates.length === 0 - ) - return b.id - a.id; - if (a.statusReportUpdates.length === 0) return -1; - if (b.statusReportUpdates.length === 0) return 1; - return ( - b.statusReportUpdates[0].date.getTime() - - a.statusReportUpdates[0].date.getTime() || b.id - a.id - ); - }); + const statusReports = _page.statusReports.sort( + sortStatusReportsByLatestUpdate, + ); const maintenances = _page.maintenances.sort( (a, b) => b.from.getTime() - a.from.getTime(), @@ -675,12 +678,16 @@ export const statusPageRouter = createTRPCRouter({ ? _page.customTheme : null; + const statusReports = _page.statusReports.sort( + sortStatusReportsByLatestUpdate, + ); + return selectPublicPageLightSchemaWithRelation.parse({ ..._page, customTheme, monitors, incidents, - statusReports: _page.statusReports, + statusReports, maintenances: _page.maintenances, pageComponents: _page.pageComponents, pageComponentGroups: _page.pageComponentGroups, -- 2.54.0