From 940e6c024fefe5f7c09776f92aa8d92baeef845c Mon Sep 17 00:00:00 2001 From: Maximilian Kaske <56969857+mxkaske@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:00:32 +0200 Subject: [PATCH] chore: planned maintenance label (#2421) * chore: planned maintenance label * fix: ts * test: status label --- .../[locale]/(public)/feed/[type]/route.ts | 15 ++++--------- packages/emails/emails/status-report.tsx | 10 +++------ packages/emails/package.json | 1 + .../emails/src/client.integration.test.tsx | 22 +++++++++---------- packages/emails/src/client.tsx | 8 ++++--- .../src/channels/slack-blocks.ts | 13 +++-------- .../subscriptions/src/channels/webhook.ts | 6 ++--- packages/utils/package.json | 3 ++- packages/utils/src/index.ts | 1 + packages/utils/src/status.test.ts | 20 +++++++++++++++++ packages/utils/src/status.ts | 18 +++++++++++++++ pnpm-lock.yaml | 3 +++ 12 files changed, 74 insertions(+), 46 deletions(-) create mode 100644 packages/utils/src/status.test.ts create mode 100644 packages/utils/src/status.ts diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/feed/[type]/route.ts b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/feed/[type]/route.ts index f8ea2279c..4208f5333 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/feed/[type]/route.ts +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/feed/[type]/route.ts @@ -1,3 +1,4 @@ +import { statusLabel } from "@openstatus/utils"; import { Feed } from "feed"; import { notFound, unauthorized } from "next/navigation"; @@ -5,14 +6,6 @@ import { auth } from "../../../../../../../lib/auth"; import { getBaseUrl } from "../../../../../../../lib/base-url"; import { getQueryClient, trpc } from "../../../../../../../lib/trpc/server"; -const STATUS_LABELS = { - investigating: "Investigating", - identified: "Identified", - monitoring: "Monitoring", - resolved: "Resolved", - maintenance: "Maintenance", -} as const; - export const revalidate = 60; export async function GET( @@ -90,7 +83,7 @@ export async function GET( const maintenanceUrl = `${baseUrl}/events/maintenance/${maintenance.id}`; feed.addItem({ id: maintenanceUrl, - title: `Maintenance - ${maintenance.title}`, + title: `${statusLabel("maintenance")} - ${maintenance.title}`, link: maintenanceUrl, description: maintenance.message, date: maintenance.updatedAt ?? maintenance.createdAt ?? new Date(), @@ -99,10 +92,10 @@ export async function GET( for (const statusReport of page.statusReports ?? []) { const statusReportUrl = `${baseUrl}/events/report/${statusReport.id}`; - const status = STATUS_LABELS[statusReport.status] ?? statusReport.status; + const status = statusLabel(statusReport.status); const statusReportUpdates = (statusReport.statusReportUpdates ?? []) .map((update) => { - const updateStatus = STATUS_LABELS[update.status] ?? update.status; + const updateStatus = statusLabel(update.status); return `${updateStatus}: ${update.message}.`; }) .join("\n\n"); diff --git a/packages/emails/emails/status-report.tsx b/packages/emails/emails/status-report.tsx index 0fe8ad192..91b8a2893 100644 --- a/packages/emails/emails/status-report.tsx +++ b/packages/emails/emails/status-report.tsx @@ -1,5 +1,6 @@ /** @jsxRuntime automatic @jsxImportSource react */ +import { statusLabel } from "@openstatus/utils"; import { Body, Column, @@ -76,13 +77,8 @@ function StatusReportEmail({ {pageTitle} - - {status} + + {statusLabel(status)} diff --git a/packages/emails/package.json b/packages/emails/package.json index 229b1c93a..fa7798ea9 100644 --- a/packages/emails/package.json +++ b/packages/emails/package.json @@ -17,6 +17,7 @@ "test": "deno test --parallel -A --no-check --sloppy-imports" }, "dependencies": { + "@openstatus/utils": "workspace:*", "@t3-oss/env-core": "catalog:", "effect": "catalog:", "react": "catalog:", diff --git a/packages/emails/src/client.integration.test.tsx b/packages/emails/src/client.integration.test.tsx index fde8e72d2..8829ab403 100644 --- a/packages/emails/src/client.integration.test.tsx +++ b/packages/emails/src/client.integration.test.tsx @@ -1,5 +1,6 @@ /** @jsxRuntime automatic @jsxImportSource react */ +import { statusLabel } from "@openstatus/utils"; import { expect } from "@std/expect"; import { describe, test } from "@std/testing/bdd"; import { render } from "react-email"; @@ -94,20 +95,21 @@ describe("Status Report Email - Subject Line", () => { ); }); - test('does not prepend "RESOLVED:" for any non-resolved status', () => { - const nonResolved = [ - "investigating", - "identified", - "monitoring", - "maintenance", - ] as const; + test("uses the bare report title for ongoing report statuses", () => { + const ongoing = ["investigating", "identified", "monitoring"] as const; - for (const status of nonResolved) { + for (const status of ongoing) { const subject = statusReportSubject({ status, reportTitle }); expect(subject).toBe(reportTitle); expect(subject).not.toContain("RESOLVED:"); } }); + + test('prepends "Planned Maintenance:" when status is "maintenance"', () => { + expect(statusReportSubject({ status: "maintenance", reportTitle })).toBe( + `Planned Maintenance: ${reportTitle}`, + ); + }); }); describe("Status Report Email - Email Content Validation", () => { @@ -156,9 +158,7 @@ describe("Status Report Email - Email Content Validation", () => { />, ); - // Should render without errors and contain the status - // Note: status is rendered lowercase in HTML with text-transform: uppercase CSS - expect(html).toContain(status); + expect(html).toContain(statusLabel(status)); } }); }); diff --git a/packages/emails/src/client.tsx b/packages/emails/src/client.tsx index d14664815..074709ec6 100644 --- a/packages/emails/src/client.tsx +++ b/packages/emails/src/client.tsx @@ -1,5 +1,6 @@ /** @jsxRuntime automatic @jsxImportSource react */ +import { statusLabel } from "@openstatus/utils"; import { type Duration, Effect, Schedule } from "effect"; import { render } from "react-email"; import { Resend } from "resend"; @@ -23,9 +24,10 @@ export function statusReportSubject(req: { status: StatusReportProps["status"]; reportTitle: string; }): string { - return req.status === "resolved" - ? `RESOLVED: ${req.reportTitle}` - : req.reportTitle; + if (req.status === "resolved") return `RESOLVED: ${req.reportTitle}`; + if (req.status === "maintenance") + return `${statusLabel("maintenance")}: ${req.reportTitle}`; + return req.reportTitle; } // Deterministic Resend rejections: retrying the identical request can never diff --git a/packages/subscriptions/src/channels/slack-blocks.ts b/packages/subscriptions/src/channels/slack-blocks.ts index be418d372..ad843de8d 100644 --- a/packages/subscriptions/src/channels/slack-blocks.ts +++ b/packages/subscriptions/src/channels/slack-blocks.ts @@ -1,4 +1,5 @@ import { COLORS } from "@openstatus/notification-base"; +import { statusLabel } from "@openstatus/utils"; import type { KnownBlock, MessageAttachment } from "@slack/web-api"; import type { PageUpdate, Subscription } from "../types"; @@ -13,14 +14,6 @@ const STATUS_EMOJI: Record = { maintenance: "🔧", }; -const STATUS_LABEL: Record = { - investigating: "Investigating", - identified: "Identified", - monitoring: "Monitoring", - resolved: "Resolved", - maintenance: "Maintenance", -}; - function statusColor(status: PageUpdate["status"]): StatusColor { switch (status) { case "investigating": @@ -61,7 +54,7 @@ export function buildRootMessage( subscription: Subscription, ): SlackRootMessage { const emoji = STATUS_EMOJI[pageUpdate.status]; - const label = STATUS_LABEL[pageUpdate.status]; + const label = statusLabel(pageUpdate.status); const origin = pageOrigin(subscription); const blocks: KnownBlock[] = [ @@ -130,7 +123,7 @@ export function buildRootMessage( export function buildReplyMessage(pageUpdate: PageUpdate): SlackReplyMessage { const emoji = STATUS_EMOJI[pageUpdate.status]; - const label = STATUS_LABEL[pageUpdate.status]; + const label = statusLabel(pageUpdate.status); const heading = `${emoji} *${label}* · ${pageUpdate.date}`; return { diff --git a/packages/subscriptions/src/channels/webhook.ts b/packages/subscriptions/src/channels/webhook.ts index c09212776..13cb72e52 100644 --- a/packages/subscriptions/src/channels/webhook.ts +++ b/packages/subscriptions/src/channels/webhook.ts @@ -1,5 +1,5 @@ import { COLORS, COLOR_DECIMALS } from "@openstatus/notification-base"; -import { assertSafeUrl } from "@openstatus/utils"; +import { assertSafeUrl, statusLabel } from "@openstatus/utils"; import { z } from "zod"; import { WEBHOOK_PAYLOAD_VERSION } from "../payload"; @@ -148,7 +148,7 @@ function buildSlackPayload( fields: [ { type: "mrkdwn", - text: `*Status*\n${pageUpdate.status}`, + text: `*Status*\n${statusLabel(pageUpdate.status)}`, }, { type: "mrkdwn", @@ -242,7 +242,7 @@ function buildDiscordPayload( fields: [ { name: "Status", - value: pageUpdate.status, + value: statusLabel(pageUpdate.status), inline: true, }, { diff --git a/packages/utils/package.json b/packages/utils/package.json index 23419d760..639732845 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -7,7 +7,8 @@ "author": "", "main": "src/index.ts", "scripts": { - "check": "deno check --sloppy-imports ." + "check": "deno check --sloppy-imports .", + "test": "deno test --parallel -A --no-check --sloppy-imports" }, "dependencies": { "@openstatus/assertions": "workspace:*", diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 381c101d3..65ae7343a 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -13,6 +13,7 @@ export { MONITOR_JOB_TYPES, } from "./constants"; export { yieldMany, iteratorToStream } from "./stream"; +export { statusLabel, type PageUpdateStatus } from "./status"; export function transformHeaders(headers: { key: string; value: string }[]) { return headers.length > 0 diff --git a/packages/utils/src/status.test.ts b/packages/utils/src/status.test.ts new file mode 100644 index 000000000..7e1eae0b9 --- /dev/null +++ b/packages/utils/src/status.test.ts @@ -0,0 +1,20 @@ +import { expect } from "@std/expect"; +import { describe, it } from "@std/testing/bdd"; + +import { type PageUpdateStatus, statusLabel } from "./status"; + +describe("statusLabel", () => { + const labels: Record = { + investigating: "Investigating", + identified: "Identified", + monitoring: "Monitoring", + resolved: "Resolved", + maintenance: "Planned Maintenance", + }; + + for (const [status, label] of Object.entries(labels)) { + it(`labels ${status} as "${label}"`, () => { + expect(statusLabel(status as PageUpdateStatus)).toBe(label); + }); + } +}); diff --git a/packages/utils/src/status.ts b/packages/utils/src/status.ts new file mode 100644 index 000000000..21b8a18e9 --- /dev/null +++ b/packages/utils/src/status.ts @@ -0,0 +1,18 @@ +export type PageUpdateStatus = + | "investigating" + | "identified" + | "monitoring" + | "resolved" + | "maintenance"; + +const STATUS_LABEL: Record = { + investigating: "Investigating", + identified: "Identified", + monitoring: "Monitoring", + resolved: "Resolved", + maintenance: "Planned Maintenance", +}; + +export function statusLabel(status: PageUpdateStatus): string { + return STATUS_LABEL[status]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 846825f83..a6804644b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1993,6 +1993,9 @@ importers: packages/emails: dependencies: + '@openstatus/utils': + specifier: workspace:* + version: link:../utils '@t3-oss/env-core': specifier: 'catalog:' version: 0.13.11(typescript@5.9.3)(zod@4.1.13) -- 2.51.2