diff --git a/client/src/Components/inputs/Dialog.tsx b/client/src/Components/inputs/Dialog.tsx
index 02d164abd..80d260a55 100644
--- a/client/src/Components/inputs/Dialog.tsx
+++ b/client/src/Components/inputs/Dialog.tsx
@@ -9,6 +9,7 @@ import { Button } from "@/Components/inputs";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
+import type { SxProps } from "@mui/material/styles";
import type { ReactNode } from "react";
export const DialogInput = ({
@@ -25,6 +26,7 @@ export const DialogInput = ({
maxWidth,
fullWidth = false,
additionalButtons,
+ cancelSx,
}: {
open: boolean;
title?: string;
@@ -39,6 +41,8 @@ export const DialogInput = ({
maxWidth?: DialogProps["maxWidth"];
fullWidth?: boolean;
additionalButtons?: ReactNode;
+ /** Styles the cancel button — e.g. an auto margin to push it left. */
+ cancelSx?: SxProps;
}) => {
const { t } = useTranslation();
const theme = useTheme();
@@ -94,6 +98,7 @@ export const DialogInput = ({
loading={loading}
variant="outlined"
onClick={onCancel}
+ sx={cancelSx}
>
{cancelText ?? t("common.buttons.cancel")}
diff --git a/client/src/Pages/Dashboard/Components/CardPicker.tsx b/client/src/Pages/Dashboard/Components/CardPicker.tsx
index e60653cac..2ff927142 100644
--- a/client/src/Pages/Dashboard/Components/CardPicker.tsx
+++ b/client/src/Pages/Dashboard/Components/CardPicker.tsx
@@ -5,7 +5,7 @@ import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
-import { Dialog, Checkbox, Button } from "@/Components/inputs";
+import { Dialog, Checkbox } from "@/Components/inputs";
import { LAYOUT } from "@/Utils/Theme/constants";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { CardGroups, type CardDefinition, type CardGroup, type CardId } from "../cards";
@@ -52,21 +52,17 @@ export const CardPicker = ({
maxWidth="sm"
fullWidth
title={t("pages.dashboard.picker.title")}
- // Every change applies immediately, so there is nothing to confirm and
- // no `onConfirm` — which also keeps the prominent contained button out
- // of the dialog entirely. Reset is destructive and stays quiet beside
- // the close action rather than being the loudest thing on screen.
- onCancel={onClose}
- cancelText={t("pages.dashboard.picker.done")}
- additionalButtons={
-
- }
+ // The dialog lays its actions out as cancel → additional → confirm, all
+ // right-aligned. Reset takes the cancel slot with an auto right margin
+ // so it sits alone on the left, away from Done — a destructive action
+ // should not be adjacent to the one people reach for by reflex.
+ onCancel={onReset}
+ cancelText={t("pages.dashboard.picker.reset")}
+ cancelSx={{ marginRight: "auto" }}
+ // Changes apply immediately, so Done only dismisses; it is the primary
+ // action here and carries the usual contained styling.
+ onConfirm={onClose}
+ confirmText={t("pages.dashboard.picker.done")}
>
- {t(`components.sidebar.menu.${group}`, { defaultValue: group })}
+ {/*
+ * The picker has its own group labels rather than reusing the
+ * sidebar's: the Logs page hosts logs, queue and diagnostics
+ * tabs, so "Logs" would misdescribe the three cards under it.
+ */}
+ {t(`pages.dashboard.picker.groups.${group}`, {
+ defaultValue: group,
+ })}
{cards.map((card) => (
);
};
+
+/**
+ * A label with its value on the right — the shape almost every stat card
+ * repeats. `color` highlights the value alone, leaving the label quiet.
+ */
+export const CardStatLine = ({
+ label,
+ value,
+ color,
+}: {
+ label: string;
+ value: string;
+ color?: string;
+}) => {
+ const theme = useTheme();
+
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ );
+};
+
+/**
+ * A CardStatLine with a proportional bar beneath it — the ranking and
+ * breakdown row. `max` defaults to a percentage scale; pass the largest value
+ * in the set to make bars relative to each other instead.
+ *
+ * `label` may be a node so callers can put a "N down" flag beside the name.
+ */
+export const CardMeterRow = ({
+ label,
+ value,
+ max,
+ color,
+ trailing,
+}: {
+ label: ReactNode;
+ value: number;
+ max?: number;
+ color: string;
+ trailing?: ReactNode;
+}) => {
+ const theme = useTheme();
+
+ return (
+
+
+ {typeof label === "string" ? (
+
+ {label}
+
+ ) : (
+ label
+ )}
+ {trailing}
+
+
+
+ );
+};
diff --git a/client/src/Pages/Dashboard/Components/Cards/AdminCards.tsx b/client/src/Pages/Dashboard/Components/Cards/AdminCards.tsx
index da00bad05..3c319fed5 100644
--- a/client/src/Pages/Dashboard/Components/Cards/AdminCards.tsx
+++ b/client/src/Pages/Dashboard/Components/Cards/AdminCards.tsx
@@ -9,7 +9,7 @@ import { LAYOUT } from "@/Utils/Theme/constants";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { formatTimestamp } from "@/Utils/TimeUtils";
import { DashboardCard, CardMessage } from "../DashboardCard";
-import { CardFigure, CardRow, CardRowLabel } from "../CardPrimitives";
+import { CardFigure, CardRow, CardRowLabel, CardStatLine } from "../CardPrimitives";
import { REFRESH_INTERVAL_MS, SLOW_REFRESH_INTERVAL_MS } from "../../cards";
@@ -19,39 +19,6 @@ import type { User } from "@/Types/User";
const MAX_ERROR_ROWS = 5;
-const StatLine = ({
- label,
- value,
- color,
-}: {
- label: string;
- value: string;
- color?: string;
-}) => {
- const theme = useTheme();
- return (
-
-
- {label}
-
-
- {value}
-
-
- );
-};
-
/** Reads as a single reassurance line while nothing is failing. */
export const ChecksOnScheduleCard = () => {
const theme = useTheme();
@@ -107,11 +74,11 @@ export const ChecksOnScheduleCard = () => {
>
)}
-
-
@@ -171,7 +138,7 @@ export const RecentErrorsCard = () => {
) : (
-
@@ -238,14 +205,14 @@ export const TeamCard = () => {
caption={t("pages.dashboard.cards.team.members", { count: total })}
/>
{byRole.map(([role, count]) => (
-
))}
{unverified > 0 && (
- {
+ const theme = useTheme();
+ const { t } = useTranslation();
+
+ // Bars are relative to the largest bucket, not to the total, so a fleet of
+ // one dominant type still shows readable proportions.
+ const max = buckets[0]?.total ?? 0;
+
+ return (
+
+ {buckets.length === 0 ? (
+
+ ) : (
+
+ {buckets.map((bucket) => (
+
+ 0 ? theme.palette.error.main : theme.palette.primary.main
+ }
+ trailing={
+
+ {bucket.down > 0 && (
+
+ {t("pages.dashboard.cards.monitorsByType.down", {
+ count: bucket.down,
+ })}
+
+ )}
+
+ {bucket.total}
+
+
+ }
+ />
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/client/src/Pages/Dashboard/Components/Cards/CheckmateServerCard.tsx b/client/src/Pages/Dashboard/Components/Cards/CheckmateServerCard.tsx
index cdc7b8b2d..9219ebbc5 100644
--- a/client/src/Pages/Dashboard/Components/Cards/CheckmateServerCard.tsx
+++ b/client/src/Pages/Dashboard/Components/Cards/CheckmateServerCard.tsx
@@ -1,14 +1,12 @@
import Stack from "@mui/material/Stack";
-import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { useGet } from "@/Hooks/UseApi";
import { LAYOUT } from "@/Utils/Theme/constants";
-import { typographyLevels } from "@/Utils/Theme/Palette";
import { formatDuration } from "@/Utils/TimeUtils";
import { DashboardCard } from "../DashboardCard";
-import { CardBar } from "../CardPrimitives";
+import { CardBar, CardStatLine } from "../CardPrimitives";
import { REFRESH_INTERVAL_MS } from "../../cards";
@@ -22,39 +20,6 @@ const EVENT_LOOP_WARM_MS = 50;
const EVENT_LOOP_HOT_MS = 200;
const HEAP_WARM_RATIO = 0.8;
-const MetricLine = ({
- label,
- value,
- color,
-}: {
- label: string;
- value: string;
- color?: string;
-}) => {
- const theme = useTheme();
- return (
-
-
- {label}
-
-
- {value}
-
-
- );
-};
-
/**
* The one card that is never empty — it reports on Checkmate itself, so it
* works on a fresh install with zero monitors. Admin only.
@@ -93,7 +58,7 @@ export const CheckmateServerCard = () => {
{data && (
- {
color={heapColor}
/>
-
-
- > = {
@@ -115,53 +111,70 @@ export const CurrentlyDownCard = () => {
color={getStatusColor(monitor.status, theme)}
size="md"
/>
-
-
- {monitor.type}
-
- {since && (
-
- {formatDuration(Date.now() - new Date(since).getTime())}
-
- )}
-
-
-
{/*
- * Plain text rather than StatusCodeLabel: that component wraps
- * its body in a Tooltip, and a focusable element nested inside
- * the row's link is unreachable by keyboard — tabbing lands on
- * the link and activating it navigates away.
+ * Two lines rather than one wide row: at half width seven
+ * columns would crush the monitor name, so the name and how long
+ * it has been down lead, and the supporting detail sits beneath
+ * in a quieter line.
*/}
-
- {formatStatusCode(latestCheck(monitor)?.statusCode, t)}
-
+
+
+ {monitor.name}
+
+ {since && (
+
+ {formatDuration(Date.now() - new Date(since).getTime())}
+
+ )}
+
+
+ {/*
+ * Plain text rather than StatusCodeLabel: that component
+ * wraps its body in a Tooltip, and a focusable element
+ * inside the row's link is unreachable by keyboard.
+ */}
+
+ {monitor.type} ·{" "}
+ {formatStatusCode(latestCheck(monitor)?.statusCode, t)}
+
+
+
+
+
+
))}
diff --git a/client/src/Pages/Dashboard/Components/Cards/MonitorsByGroupCard.tsx b/client/src/Pages/Dashboard/Components/Cards/MonitorsByGroupCard.tsx
index 215771cf3..c620a7651 100644
--- a/client/src/Pages/Dashboard/Components/Cards/MonitorsByGroupCard.tsx
+++ b/client/src/Pages/Dashboard/Components/Cards/MonitorsByGroupCard.tsx
@@ -1,36 +1,22 @@
-import Stack from "@mui/material/Stack";
-import Typography from "@mui/material/Typography";
-import { useTheme } from "@mui/material/styles";
-import { useTranslation } from "react-i18next";
import { useMemo } from "react";
-import { LAYOUT } from "@/Utils/Theme/constants";
-import { typographyLevels } from "@/Utils/Theme/Palette";
-import { DashboardCard, CardMessage } from "../DashboardCard";
-import { CardBar, CardRow } from "../CardPrimitives";
+import { BreakdownCard, type BreakdownBucket } from "./BreakdownCard";
import { useMonitors } from "../../useDashboardData";
-interface GroupBucket {
- group: string;
- total: number;
- down: number;
-}
-
export const MonitorsByGroupCard = () => {
- const theme = useTheme();
- const { t } = useTranslation();
const { data, isLoading, isValidating, error } = useMonitors();
// `group` is a plain string | null on the monitor — there is no groups
// endpoint, so the set is whatever monitors actually declare.
- const buckets = useMemo(() => {
- const byGroup = new Map();
+ const buckets = useMemo(() => {
+ const byGroup = new Map();
for (const monitor of data?.monitors ?? []) {
if (!monitor.group) {
continue;
}
const bucket = byGroup.get(monitor.group) ?? {
- group: monitor.group,
+ key: monitor.group,
+ label: monitor.group,
total: 0,
down: 0,
};
@@ -43,78 +29,15 @@ export const MonitorsByGroupCard = () => {
return [...byGroup.values()].sort((a, b) => b.total - a.total);
}, [data]);
- const max = buckets[0]?.total ?? 0;
-
return (
-
- {buckets.length === 0 ? (
-
- ) : (
-
- {buckets.map((bucket) => (
-
-
-
-
- {bucket.group}
-
-
- {bucket.down > 0 && (
-
- {t("pages.dashboard.cards.monitorsByType.down", {
- count: bucket.down,
- })}
-
- )}
-
- {bucket.total}
-
-
-
- 0
- ? theme.palette.error.main
- : theme.palette.primary.main
- }
- />
-
-
- ))}
-
- )}
-
+ error={error}
+ />
);
};
diff --git a/client/src/Pages/Dashboard/Components/Cards/MonitorsByTypeCard.tsx b/client/src/Pages/Dashboard/Components/Cards/MonitorsByTypeCard.tsx
index a36c87d0b..a42b9c4cb 100644
--- a/client/src/Pages/Dashboard/Components/Cards/MonitorsByTypeCard.tsx
+++ b/client/src/Pages/Dashboard/Components/Cards/MonitorsByTypeCard.tsx
@@ -1,37 +1,23 @@
-import Stack from "@mui/material/Stack";
-import Box from "@mui/material/Box";
-import Typography from "@mui/material/Typography";
-import { useTheme } from "@mui/material/styles";
-import { useTranslation } from "react-i18next";
import { useMemo } from "react";
-import { LAYOUT } from "@/Utils/Theme/constants";
-import { typographyLevels } from "@/Utils/Theme/Palette";
import { getMonitorPath } from "@/Utils/MonitorUtils";
-import { DashboardCard, CardMessage } from "../DashboardCard";
-import { CardBar, CardRow } from "../CardPrimitives";
+import { BreakdownCard, type BreakdownBucket } from "./BreakdownCard";
import { useMonitors } from "../../useDashboardData";
import type { MonitorType } from "@/Types/Monitor";
-interface TypeBucket {
- type: MonitorType;
- total: number;
- down: number;
-}
-
export const MonitorsByTypeCard = () => {
- const theme = useTheme();
- const { t } = useTranslation();
const { data, isLoading, isValidating, error } = useMonitors();
- const buckets = useMemo(() => {
- const byType = new Map();
+ const buckets = useMemo(() => {
+ const byType = new Map();
for (const monitor of data?.monitors ?? []) {
const bucket = byType.get(monitor.type) ?? {
- type: monitor.type,
+ key: monitor.type,
+ label: monitor.type,
total: 0,
down: 0,
+ to: `/${getMonitorPath(monitor.type)}`,
};
bucket.total += 1;
if (monitor.status === "down" || monitor.status === "breached") {
@@ -42,81 +28,15 @@ export const MonitorsByTypeCard = () => {
return [...byType.values()].sort((a, b) => b.total - a.total);
}, [data]);
- const max = buckets[0]?.total ?? 0;
-
return (
-
- {buckets.length === 0 ? (
-
- ) : (
-
- {buckets.map((bucket) => (
-
-
-
-
- {bucket.type}
-
-
- {bucket.down > 0 && (
-
- {t("pages.dashboard.cards.monitorsByType.down", {
- count: bucket.down,
- })}
-
- )}
-
- {bucket.total}
-
-
-
-
- 0
- ? theme.palette.error.main
- : theme.palette.primary.main
- }
- />
-
-
-
- ))}
-
- )}
-
+ error={error}
+ />
);
};
diff --git a/client/src/Pages/Dashboard/Components/Cards/RankingCards.tsx b/client/src/Pages/Dashboard/Components/Cards/RankingCards.tsx
index d405c5392..20938a615 100644
--- a/client/src/Pages/Dashboard/Components/Cards/RankingCards.tsx
+++ b/client/src/Pages/Dashboard/Components/Cards/RankingCards.tsx
@@ -8,7 +8,7 @@ import { LAYOUT } from "@/Utils/Theme/constants";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { getMonitorPath, getUptimePercentageColor } from "@/Utils/MonitorUtils";
import { DashboardCard, CardMessage } from "../DashboardCard";
-import { CardBar, CardRow, CardRowLabel } from "../CardPrimitives";
+import { CardMeterRow, CardRow, CardRowLabel } from "../CardPrimitives";
import { useMonitors } from "../../useDashboardData";
import type { Monitor } from "@/Types/Monitor";
@@ -83,18 +83,12 @@ export const SlowestMonitorsCard = () => {
key={monitor.id}
to={`/${getMonitorPath(monitor.type)}/${monitor.id}`}
>
-
-
-
+ }
+ value={average}
+ max={slowest}
+ color={theme.palette.primary.main}
+ trailing={
{
>
{Math.round(average)} ms
-
-
-
+ }
+ />
))}
@@ -166,18 +155,11 @@ export const LowestUptimeCard = () => {
key={monitor.id}
to={`/${getMonitorPath(monitor.type)}/${monitor.id}`}
>
-
-
-
+ }
+ value={percentage}
+ color={theme.palette[palette].main}
+ trailing={
{
>
{percentage.toFixed(1)}%
-
-
-
+ }
+ />
);
})}
diff --git a/client/src/Pages/Dashboard/Components/Cards/SummaryCards.tsx b/client/src/Pages/Dashboard/Components/Cards/SummaryCards.tsx
index 8d5b49fad..f627ea122 100644
--- a/client/src/Pages/Dashboard/Components/Cards/SummaryCards.tsx
+++ b/client/src/Pages/Dashboard/Components/Cards/SummaryCards.tsx
@@ -10,39 +10,13 @@ import { LAYOUT } from "@/Utils/Theme/constants";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { formatTimestamp } from "@/Utils/TimeUtils";
import { DashboardCard, CardMessage } from "../DashboardCard";
-import { CardFigure, CardRow, CardRowLabel } from "../CardPrimitives";
+import { CardFigure, CardRow, CardRowLabel, CardStatLine } from "../CardPrimitives";
import type { IncidentSummary } from "@/Types/Incident";
import type { Notification } from "@/Types/Notification";
import type { MaintenanceWindow } from "@/Types/MaintenanceWindow";
import type { StatusPage } from "@/Types/StatusPage";
-const StatLine = ({ label, value }: { label: string; value: string }) => {
- const theme = useTheme();
- return (
-
-
- {label}
-
-
- {value}
-
-
- );
-};
-
/** Free when Recent incidents is present — same summary call, shared by SWR. */
export const IncidentStatsCard = () => {
const theme = useTheme();
@@ -68,14 +42,14 @@ export const IncidentStatsCard = () => {
data.totalActive > 0 ? theme.palette.error.main : theme.palette.text.primary
}
/>
-
{data.topMonitor?.monitorName && (
-
@@ -115,7 +89,7 @@ export const NotificationChannelsCard = () => {
) : (
{byType.map(([type, count]) => (
- {
caption={t("pages.dashboard.cards.maintenance.runningNow")}
/>
{next && (
-
diff --git a/client/src/Pages/Dashboard/cards.ts b/client/src/Pages/Dashboard/cards.ts
index f16a61407..d9fafa320 100644
--- a/client/src/Pages/Dashboard/cards.ts
+++ b/client/src/Pages/Dashboard/cards.ts
@@ -1,13 +1,17 @@
import type { ComponentType } from "react";
-// The 12-column grid the dashboard lays cards out on. A card declares the
-// minimum number of columns it needs; the grid may stretch the last card in a
-// short row up to STRETCH_MAX_WIDTH. Declared width is never a user setting.
-export const CardWidths = [4, 6, 8, 12] as const;
+// The 12-column grid the dashboard lays cards out on.
+//
+// Only two widths exist. Cards are half-width by default and pair up down the
+// page; a card is full-width only when its content genuinely needs the room,
+// which makes the wide cards read as a deliberate signal rather than as an
+// inconsistent grid. Width is declared per card type in code and is never a
+// user setting.
+export const CardWidths = [6, 12] as const;
export type CardWidth = (typeof CardWidths)[number];
export const GRID_COLUMNS = 12;
-export const STRETCH_MAX_WIDTH = 8;
+export const HALF_WIDTH: CardWidth = 6;
// Picker groups mirror the sidebar sections: you look for a card where you
// look for the feature it belongs to.
diff --git a/client/src/Pages/Dashboard/layout.ts b/client/src/Pages/Dashboard/layout.ts
index 509f1ed26..5e9d2efe5 100644
--- a/client/src/Pages/Dashboard/layout.ts
+++ b/client/src/Pages/Dashboard/layout.ts
@@ -1,57 +1,40 @@
-import { GRID_COLUMNS, STRETCH_MAX_WIDTH, type CardDefinition } from "./cards";
+import { GRID_COLUMNS, HALF_WIDTH, type CardDefinition } from "./cards";
export interface PlacedCard {
card: CardDefinition;
- /** Columns actually rendered — the declared width, or more if stretched. */
+ /** Columns rendered. Equal to the declared width except for a lone half. */
renderedWidth: number;
}
/**
- * Packs cards into rows of GRID_COLUMNS. Declared width is a minimum; when a
- * row ends short, its last card absorbs the remainder.
+ * Packs cards into rows of GRID_COLUMNS.
*
- * A row in the middle of the page always fills completely — leaving a hole
- * there reads as a rendering fault. Only the final row may end short, and only
- * when a single card sits in it: stretching a lone trailing card to full width
- * makes a 4-column card look like a banner. There, STRETCH_MAX_WIDTH caps the
- * growth instead.
+ * With only two widths this is nearly trivial: two halves fill a row exactly
+ * and a full-width card fills its own, so no card is ever stretched to close a
+ * gap. The one case needing a decision is a half-width card left alone at the
+ * end of the page — it is promoted to full width so the page does not end on a
+ * ragged half.
*
- * `grid-auto-flow: dense` was tried instead and does not help: no later card is
- * small enough to backfill the gaps that occur in practice.
+ * `grid-auto-flow: dense` was tried and does not help: with a single body width
+ * there is never a smaller card available to backfill.
*/
export const layoutCards = (cards: CardDefinition[]): PlacedCard[] => {
- const placed: PlacedCard[] = [];
- let row: PlacedCard[] = [];
- let used = 0;
+ const placed: PlacedCard[] = cards.map((card) => ({
+ card,
+ renderedWidth: card.width,
+ }));
- const flush = (isFinalRow: boolean) => {
- if (row.length === 0) {
- return;
- }
- const remainder = GRID_COLUMNS - used;
- if (remainder > 0) {
- const last = row[row.length - 1];
- // A lone card ending the page grows only to the cap; every other short
- // row is filled completely so no gap is left mid-page.
- const ceiling = isFinalRow && row.length === 1 ? STRETCH_MAX_WIDTH : GRID_COLUMNS;
- last.renderedWidth = Math.min(last.renderedWidth + remainder, ceiling);
- }
- placed.push(...row);
- row = [];
- used = 0;
- };
+ // Columns occupied in the row still open once every card is placed. A
+ // trailing half-width card leaves exactly HALF_WIDTH here.
+ const trailing = placed.reduce(
+ (used, entry) => (used + entry.renderedWidth) % GRID_COLUMNS,
+ 0
+ );
- for (const card of cards) {
- if (used + card.width > GRID_COLUMNS) {
- flush(false);
- }
- row.push({ card, renderedWidth: card.width });
- used += card.width;
- if (used === GRID_COLUMNS) {
- flush(false);
- }
+ const last = placed[placed.length - 1];
+ if (last && trailing === HALF_WIDTH) {
+ last.renderedWidth = GRID_COLUMNS;
}
- flush(true);
return placed;
};
diff --git a/client/src/Pages/Dashboard/registry.ts b/client/src/Pages/Dashboard/registry.ts
index b8a87ea27..2aaddb125 100644
--- a/client/src/Pages/Dashboard/registry.ts
+++ b/client/src/Pages/Dashboard/registry.ts
@@ -24,10 +24,10 @@ import type { CardDefinition, CardId } from "./cards";
// Declared width is the minimum number of columns a card needs. The grid may
// stretch the last card of a short row; nothing here is a user setting.
//
-// Order is the render order, and the order the picker lists them in. Cards are
-// sequenced so their declared widths pair into full rows: a card left alone in
-// a row gets stretched to fill it, so an 8-wide card is kept next to a 4-wide
-// one rather than being widened back to 12.
+// Order is the render order, and the order the picker lists them in.
+//
+// Full width is reserved for Monitor status alone, which lays six counts across
+// a single row. Every other card is half-width and pairs up down the page.
export const CARD_REGISTRY: CardDefinition[] = [
{
id: "monitorStatus",
@@ -40,16 +40,14 @@ export const CARD_REGISTRY: CardDefinition[] = [
id: "currentlyDown",
key: "currentlyDown",
group: "uptime",
- // Six columns per row — the densest card — but it does not need the
- // full width, and 8 leaves room for a 4-wide card beside it.
- width: 8,
+ width: 6,
component: CurrentlyDownCard,
},
{
id: "failedChecks",
key: "failedChecks",
group: "checks",
- width: 4,
+ width: 6,
component: FailedChecksCard,
},
{
@@ -91,42 +89,42 @@ export const CARD_REGISTRY: CardDefinition[] = [
id: "recentIncidents",
key: "recentIncidents",
group: "incidents",
- width: 4,
+ width: 6,
component: RecentIncidentsCard,
},
{
id: "incidentStats",
key: "incidentStats",
group: "incidents",
- width: 4,
+ width: 6,
component: IncidentStatsCard,
},
{
id: "notificationChannels",
key: "notificationChannels",
group: "notifications",
- width: 4,
+ width: 6,
component: NotificationChannelsCard,
},
{
id: "maintenance",
key: "maintenance",
group: "maintenance",
- width: 4,
+ width: 6,
component: MaintenanceCard,
},
{
id: "statusPages",
key: "statusPages",
group: "statusPages",
- width: 4,
+ width: 6,
component: StatusPagesCard,
},
{
id: "checkmateServer",
key: "checkmateServer",
group: "logs",
- width: 4,
+ width: 6,
adminOnly: true,
component: CheckmateServerCard,
},
@@ -134,7 +132,7 @@ export const CARD_REGISTRY: CardDefinition[] = [
id: "checksOnSchedule",
key: "checksOnSchedule",
group: "logs",
- width: 4,
+ width: 6,
adminOnly: true,
component: ChecksOnScheduleCard,
},
@@ -142,7 +140,7 @@ export const CARD_REGISTRY: CardDefinition[] = [
id: "recentErrors",
key: "recentErrors",
group: "logs",
- width: 4,
+ width: 6,
adminOnly: true,
component: RecentErrorsCard,
},
@@ -150,7 +148,7 @@ export const CARD_REGISTRY: CardDefinition[] = [
id: "team",
key: "team",
group: "settings",
- width: 4,
+ width: 6,
adminOnly: true,
component: TeamCard,
},
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 9ea65b301..2c3967e2e 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -323,7 +323,7 @@
"empty": "No channels yet."
},
"maintenance": {
- "title": "Maintenance",
+ "title": "Maintenance windows",
"empty": "No maintenance scheduled.",
"runningNow": "Running now",
"next": "Next window"
@@ -336,7 +336,7 @@
"draft": "Draft"
},
"checksOnSchedule": {
- "title": "Checks on schedule",
+ "title": "Check queue",
"healthy": "All {{jobs}} jobs on schedule across {{workers}} worker(s).",
"failing": "Failing jobs",
"scheduled": "Scheduled jobs",
@@ -349,7 +349,7 @@
"warnings": "Warnings"
},
"team": {
- "title": "Team",
+ "title": "Team members",
"members": "{{count}} members",
"unverified": "Unverified"
}
@@ -358,7 +358,18 @@
"trigger": "Edit cards",
"title": "Dashboard cards",
"done": "Done",
- "reset": "Reset to defaults"
+ "reset": "Reset to defaults",
+ "groups": {
+ "uptime": "Uptime",
+ "infrastructure": "Infrastructure",
+ "incidents": "Incidents",
+ "notifications": "Notifications",
+ "maintenance": "Maintenance",
+ "statusPages": "Status pages",
+ "checks": "Checks",
+ "logs": "System",
+ "settings": "Settings"
+ }
},
"empty": {
"title": "No cards on your dashboard",