diff --git a/client/src/Pages/StatusPage/Status/themes/bold/styles.ts b/client/src/Pages/StatusPage/Status/themes/bold/styles.ts index 86208d41b..197310a7f 100644 --- a/client/src/Pages/StatusPage/Status/themes/bold/styles.ts +++ b/client/src/Pages/StatusPage/Status/themes/bold/styles.ts @@ -1,5 +1,5 @@ import type { SxProps, Theme } from "@mui/material/styles"; -import type { StatusPageThemeTokens } from "../tokens"; +import { mixSeverityColor, type StatusPageThemeTokens } from "../tokens"; import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus"; import { BOLD_SANS_STACK, MONO_STACK } from "../shared/fontStacks"; import { MAX_RECENT_CHECKS } from "@/Types/Monitor"; @@ -34,9 +34,9 @@ export interface BoldStyles { monitorUrl: SxProps; badge: (tone: OverallTone) => SxProps; heatmap: SxProps; - heatmapCell: (kind: BoldHeatCell) => SxProps; + heatmapCell: (kind: BoldHeatCell, severity?: number) => SxProps; histogram: SxProps; - bar: (kind: BoldBarKind, heightPct: number) => SxProps; + bar: (kind: BoldBarKind, heightPct: number, severity?: number) => SxProps; chartStats: SxProps; infra: SxProps; infraEmpty: SxProps; @@ -327,9 +327,12 @@ export const boldStyles = ( gap: { xs: "1px", md: "3px" }, height: 48, }, - heatmapCell: (kind) => ({ + heatmapCell: (kind, severity = 1) => ({ borderRadius: "3px", - background: heatCellBg[kind], + background: + kind === "degraded" + ? mixSeverityColor(tokens.up, tokens.degraded, severity) + : heatCellBg[kind], boxShadow: heatCellShadow[kind], opacity: kind === "empty" ? 0.4 : 1, transition: "transform 0.15s", @@ -344,8 +347,11 @@ export const boldStyles = ( alignItems: "flex-end", height: 48, }, - bar: (kind, heightPct) => ({ - background: barBg[kind], + bar: (kind, heightPct, severity = 1) => ({ + background: + kind === "degraded" + ? mixSeverityColor(tokens.up, tokens.degraded, severity) + : barBg[kind], borderRadius: "3px", minHeight: 4, opacity: kind === "empty" ? 0.4 : 1, diff --git a/client/src/Pages/StatusPage/Status/themes/editorial/styles.ts b/client/src/Pages/StatusPage/Status/themes/editorial/styles.ts index 9de2c96fb..759f856a0 100644 --- a/client/src/Pages/StatusPage/Status/themes/editorial/styles.ts +++ b/client/src/Pages/StatusPage/Status/themes/editorial/styles.ts @@ -1,5 +1,5 @@ import type { SxProps, Theme } from "@mui/material/styles"; -import type { StatusPageThemeTokens } from "../tokens"; +import { mixSeverityColor, type StatusPageThemeTokens } from "../tokens"; import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus"; import { EDITORIAL_SECONDARY_SANS_STACK, @@ -38,9 +38,9 @@ export interface EditorialStyles { monitorUrl: SxProps; badge: (tone: OverallTone) => SxProps; heatmap: SxProps; - heatmapCell: (kind: EditorialHeatCell) => SxProps; + heatmapCell: (kind: EditorialHeatCell, severity?: number) => SxProps; histogram: SxProps; - bar: (kind: EditorialBarKind, heightPct: number) => SxProps; + bar: (kind: EditorialBarKind, heightPct: number, severity?: number) => SxProps; chartStats: SxProps; infra: SxProps; infraEmpty: SxProps; @@ -272,8 +272,11 @@ export const editorialStyles = ( gap: { xs: "1px", md: "2px" }, height: 40, }, - heatmapCell: (kind) => ({ - background: heatCellBg[kind], + heatmapCell: (kind, severity = 1) => ({ + background: + kind === "degraded" + ? mixSeverityColor(tokens.up, tokens.degraded, severity) + : heatCellBg[kind], opacity: kind === "empty" ? 0.5 : 1, }), @@ -285,8 +288,11 @@ export const editorialStyles = ( alignItems: "flex-end", height: 40, }, - bar: (kind, heightPct) => ({ - background: barBg[kind], + bar: (kind, heightPct, severity = 1) => ({ + background: + kind === "degraded" + ? mixSeverityColor(tokens.up, tokens.degraded, severity) + : barBg[kind], minHeight: 3, opacity: kind === "empty" ? 0.5 : 1, height: `${heightPct}%`, diff --git a/client/src/Pages/StatusPage/Status/themes/modern/styles.ts b/client/src/Pages/StatusPage/Status/themes/modern/styles.ts index 7bb108f51..710da1777 100644 --- a/client/src/Pages/StatusPage/Status/themes/modern/styles.ts +++ b/client/src/Pages/StatusPage/Status/themes/modern/styles.ts @@ -1,6 +1,6 @@ import type { SxProps, Theme } from "@mui/material/styles"; import { keyframes } from "@mui/system"; -import type { StatusPageThemeTokens } from "../tokens"; +import { mixSeverityColor, type StatusPageThemeTokens } from "../tokens"; import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus"; import { MONO_STACK, SANS_STACK } from "../shared/fontStacks"; import { MAX_RECENT_CHECKS } from "@/Types/Monitor"; @@ -37,9 +37,9 @@ export interface ModernStyles { monitorUrl: SxProps; badge: (tone: OverallTone) => SxProps; heatmap: SxProps; - heatmapCell: (kind: ModernHeatCell) => SxProps; + heatmapCell: (kind: ModernHeatCell, severity?: number) => SxProps; histogram: SxProps; - bar: (kind: ModernBarKind, heightPct: number) => SxProps; + bar: (kind: ModernBarKind, heightPct: number, severity?: number) => SxProps; chartStats: SxProps; infra: SxProps; infraEmpty: SxProps; @@ -83,13 +83,14 @@ export const modernStyles = ( ): ModernStyles => { const cardShadow = isDark ? darkShadow : lightShadow; const cardHoverShadow = isDark ? darkShadowHover : lightShadowHover; + const darken = (color: string) => `color-mix(in srgb, ${color} 70%, #000000 30%)`; const heatCellBg: Record = { fast: `linear-gradient(180deg, ${tokens.up}, ${tokens.upStrong})`, med: `linear-gradient(180deg, color-mix(in srgb, ${tokens.up} 70%, #ffffff 30%), ${tokens.up})`, slow: `linear-gradient(180deg, ${tokens.warn}, ${tokens.degraded})`, - degraded: `linear-gradient(180deg, ${tokens.degraded}, color-mix(in srgb, ${tokens.degraded} 70%, #000000 30%))`, - down: `linear-gradient(180deg, ${tokens.down}, color-mix(in srgb, ${tokens.down} 70%, #000000 30%))`, + degraded: `linear-gradient(180deg, ${tokens.degraded}, ${darken(tokens.degraded)})`, + down: `linear-gradient(180deg, ${tokens.down}, ${darken(tokens.down)})`, empty: tokens.border, }; @@ -103,7 +104,7 @@ export const modernStyles = ( const gaugeFillBg: Record = { ok: `linear-gradient(90deg, ${tokens.up}, ${tokens.upStrong})`, warm: `linear-gradient(90deg, ${tokens.warn}, ${tokens.degraded})`, - hot: `linear-gradient(90deg, ${tokens.down}, color-mix(in srgb, ${tokens.down} 70%, #000000 30%))`, + hot: `linear-gradient(90deg, ${tokens.down}, color-mix(in srgb, ${tokens.down}, ${darken(tokens.down)})`, }; return { @@ -354,13 +355,19 @@ export const modernStyles = ( gap: { xs: "1px", md: "3px" }, height: 46, }, - heatmapCell: (kind) => ({ - borderRadius: "3px", - background: heatCellBg[kind], - opacity: kind === "empty" ? 0.4 : 1, - transition: "transform 0.15s", - "&:hover": { transform: "scaleY(1.2)" }, - }), + heatmapCell: (kind, severity = 1) => { + const mixed = mixSeverityColor(tokens.up, tokens.degraded, severity); + return { + borderRadius: "3px", + background: + kind === "degraded" + ? `linear-gradient(180deg, ${mixed}, ${darken(mixed)})` + : heatCellBg[kind], + opacity: kind === "empty" ? 0.4 : 1, + transition: "transform 0.15s", + "&:hover": { transform: "scaleY(1.2)" }, + }; + }, histogram: { padding: { xs: "0 12px", md: "0 24px" }, @@ -370,8 +377,11 @@ export const modernStyles = ( alignItems: "flex-end", height: 46, }, - bar: (kind, heightPct) => ({ - background: barBg[kind], + bar: (kind, heightPct, severity = 1) => ({ + background: + kind === "degraded" + ? mixSeverityColor(tokens.up, tokens.degraded, severity) + : barBg[kind], borderRadius: "3px", minHeight: 4, opacity: kind === "empty" ? 0.4 : 1, diff --git a/client/src/Pages/StatusPage/Status/themes/refined/styles.ts b/client/src/Pages/StatusPage/Status/themes/refined/styles.ts index 38f972c95..2655ab555 100644 --- a/client/src/Pages/StatusPage/Status/themes/refined/styles.ts +++ b/client/src/Pages/StatusPage/Status/themes/refined/styles.ts @@ -1,5 +1,5 @@ import type { SxProps, Theme } from "@mui/material/styles"; -import type { StatusPageThemeTokens } from "../tokens"; +import { mixSeverityColor, type StatusPageThemeTokens } from "../tokens"; import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus"; import { MONO_STACK, SANS_STACK } from "../shared/fontStacks"; @@ -37,9 +37,9 @@ export interface RefinedStyles { monitorUrl: SxProps; badge: (tone: OverallTone) => SxProps; heatmap: SxProps; - heatmapCell: (kind: RefinedHeatCell) => SxProps; + heatmapCell: (kind: RefinedHeatCell, severity?: number) => SxProps; histogram: SxProps; - bar: (kind: RefinedBarKind, heightPct: number) => SxProps; + bar: (kind: RefinedBarKind, heightPct: number, severity?: number) => SxProps; chartStats: SxProps; infra: SxProps; infraEmpty: SxProps; @@ -296,9 +296,12 @@ export const refinedStyles = ( gap: { xs: "1px", md: "3px" }, height: 42, }, - heatmapCell: (kind) => ({ + heatmapCell: (kind, severity = 1) => ({ borderRadius: "2px", - background: heatCellBg[kind], + background: + kind === "degraded" + ? mixSeverityColor(tokens.up, tokens.degraded, severity) + : heatCellBg[kind], opacity: kind === "empty" ? 0.4 : 1, transition: "transform 0.15s", "&:hover": { transform: "scaleY(1.15)" }, @@ -312,8 +315,11 @@ export const refinedStyles = ( alignItems: "flex-end", height: 42, }, - bar: (kind, heightPct) => ({ - background: barBg[kind], + bar: (kind, heightPct, severity = 1) => ({ + background: + kind === "degraded" + ? mixSeverityColor(tokens.up, tokens.degraded, severity) + : barBg[kind], borderRadius: "2px", minHeight: 3, opacity: kind === "empty" ? 0.4 : 1, diff --git a/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx b/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx index cf08ca62f..807eed742 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx @@ -59,9 +59,9 @@ export interface BaseStyles { monitorUrl: SxProps; badge: (tone: OverallTone) => SxProps; heatmap: SxProps; - heatmapCell: (kind: HeatCellKind) => SxProps; + heatmapCell: (kind: HeatCellKind, severity?: number) => SxProps; histogram: SxProps; - bar: (kind: BarKind, heightPct: number) => SxProps; + bar: (kind: BarKind, heightPct: number, severity?: number) => SxProps; chartStats: SxProps; infra: SxProps; infraEmpty: SxProps; diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ChartCells.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ChartCells.tsx index e45d2e6b6..8d8a6a675 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ChartCells.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ChartCells.tsx @@ -18,6 +18,7 @@ export type HeatCellKind = "fast" | "med" | "slow" | "degraded" | "down" | "empt export interface ChartCell { key: string; + severity: number; barKind: BarKind; heatKind: HeatCellKind; heightPct: number; // histogram bar height @@ -28,9 +29,22 @@ export interface ChartCell { const DAY_DOWN_THRESHOLD = 0.5; -const classifyDay = (bucket: DailyCheckBucket): BarKind => { +// Color interpolation +// 0 = perfect +// 1 = at or below the down threshold +const SEVERITY_FLOOR = 0.15; // A degraded server should never be green +const SEVERITY_GAMMA = 2; // >1 biases toward green so high-uptime days stay visibly distinct from low ones + +export const severityFromUpRatio = (upRatio: number): number => { + const failed = 1 - upRatio; + if (failed <= 0) return 0; + const scaled = (Math.log10(failed) + 3) / (Math.log10(1 - DAY_DOWN_THRESHOLD) + 3); + const clamped = Math.min(1, Math.max(0, scaled)); + return Math.max(SEVERITY_FLOOR, clamped ** SEVERITY_GAMMA); +}; + +const classifyDay = (bucket: DailyCheckBucket, upRatio: number): BarKind => { if (bucket.totalChecks === 0) return "empty"; - const upRatio = bucket.upChecks / bucket.totalChecks; if (upRatio >= 1) return "up"; if (upRatio < DAY_DOWN_THRESHOLD) return "down"; return "degraded"; @@ -55,6 +69,7 @@ const emptyCell = (key: string): ChartCell => ({ responseTime: 0, tooltip: null, ariaLabel: "", + severity: 0, }); const padCells = (cells: ChartCell[], length: number): ChartCell[] => [ @@ -76,6 +91,7 @@ export const checksToCells = (checks: CheckSnapshot[]): ChartCell[] => { responseTime: check.responseTime ?? 0, tooltip: , ariaLabel: `${formatMs(check.responseTime)}, ${check.status ? "up" : "down"}`, + severity: 0, }) ); return padCells(cells, MAX_RECENT_CHECKS); @@ -93,9 +109,17 @@ export const dailyBucketsToCells = ( return enumerated.map((date) => { const bucket = byDate.get(date); if (!bucket) return emptyCell(`day-${date}`); - const barKind = classifyDay(bucket); + const upRatio = bucket.upChecks / bucket.totalChecks; + + const barKind = classifyDay(bucket, upRatio); return { key: `day-${date}`, + severity: + barKind === "degraded" + ? severityFromUpRatio(upRatio) + : barKind === "down" + ? 1 + : 0, barKind, heatKind: barKind === "up" diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx index 390f70667..7ddc7424d 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx @@ -10,7 +10,7 @@ import type { interface Props { cells: ChartCell[]; containerSx: SxProps; - cellSx: (kind: HeatCellKind) => SxProps; + cellSx: (kind: HeatCellKind, severity?: number) => SxProps; } export const ThemedHeatmap = ({ cells, containerSx, cellSx }: Props) => { @@ -42,7 +42,7 @@ export const ThemedHeatmap = ({ cells, containerSx, cellSx }: Props) => { placement="top" > diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx index 3643d901c..ea5fa649a 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx @@ -13,7 +13,7 @@ import type { interface Props { cells: ChartCell[]; containerSx: SxProps; - barSx: (kind: BarKind, heightPct: number) => SxProps; + barSx: (kind: BarKind, heightPct: number, severity?: number) => SxProps; statsSx: SxProps; statsGap?: number; } @@ -61,7 +61,7 @@ export const ThemedHistogram = ({ placement="top" > diff --git a/client/src/Pages/StatusPage/Status/themes/tokens.ts b/client/src/Pages/StatusPage/Status/themes/tokens.ts index 3ec90abde..644bad8d2 100644 --- a/client/src/Pages/StatusPage/Status/themes/tokens.ts +++ b/client/src/Pages/StatusPage/Status/themes/tokens.ts @@ -243,3 +243,10 @@ export const themeTokens: Record = { editorial, minimal, }; + +// Interpolate between a theme's up and degraded colors by severity (0..1). +export const mixSeverityColor = ( + up: string, + degraded: string, + severity: number +): string => `color-mix(in oklch, ${degraded} ${Math.round(severity * 100)}%, ${up})`;