Anonymous visitors to a public status page have no persisted Redux
state, so ThemedHeatmap and ThemedHistogram tooltips always rendered
timestamps in the hardcoded "America/Toronto" default — regardless of
the timezone the operator configured. Even on the same domain,
ThemedHeatmap and ThemedHistogram had no code path to consume the
operator-configured statusPage.timezone returned by the API.
- Add SYSTEM_TIMEZONE module constant and a small resolveTimezone()
helper to TimeUtils.ts. formatDateWithTz's timezone parameter is now
optional and defaults to the system tz, so callers don't need to
rebuild the prop -> browser -> UTC chain inline.
- Add a `timezone` field to StatusPageThemeContextValue so any themed
component on the public page can read the page-wide tz without prop
drilling (parallels how `theme` and `mode` are already exposed).
- Wire statusPage.timezone into StatusPageThemeProvider from
Pages/StatusPage/Status/index.tsx (both the public route and the
admin preview).
- ThemedHeatmap and ThemedHistogram now read tz from the context
instead of useSelector(state.ui.timezone) — that selector was
unreliable for anonymous visitors and removing it cuts a Redux
subscription on the public page.
- Replace the inline `data?.timezone || Intl.DateTimeFormat()...`
expression in useStatusPageForm.ts with resolveTimezone().
Closes#3628.
Translates captureUrl.label ("Capture endpoint") and the new long
description.hardware copy into all 15 non-English locales (ar, cs, de,
es, fi, fr, ja, pt-BR, ru, th, tr, uk, vi, zh-CN, zh-TW). The literal
URL example and the token name "Bearer" are kept untranslated.
The previous commit overwrote description.hardware with English in 15
non-English locales. Restore each locale's existing translation; only
the en.json copy carries the new path/Bearer guidance. Translators will
pick up the English text on the next PoEditor sync.
The previous commit added inline helperText under the URL and Secret
fields, but no other form in the codebase uses helperText for static
guidance — only for validation errors. The project's pattern for setup
guidance is the ConfigBox subtitle text on the left side of the form,
populated from pages.createMonitor.form.general.description.<type>.
Move the guidance there so this form follows the same convention as
every other monitor type.
- Remove urlHelperText / secretHelperText from GeneralSettingsConfig
- Remove the new helperText props on the URL and Secret TextFields
- Drop captureUrl.helperText and secret.helperText keys from all
16 locales
- Beef up description.hardware to mention the /api/v1/metrics path
requirement and the no-Bearer-prefix rule
The realistic captureUrl placeholder (added in the previous commit)
stays — it does most of the URL-disambiguation work on its own.
Two real users hit the same confusion: the Capture endpoint URL needs
the full path /api/v1/metrics, and the authorization secret should not
include a "Bearer" prefix. The form previously gave no guidance on
either — same generic placeholder as HTTP monitors and no helper text.
- Replace the URL field's generic label/placeholder with hardware-
specific copy ("Capture endpoint", realistic 192.168.1.10:5001/...
example) and add helper text under the field.
- Add helper text under the authorization secret field clarifying that
only the raw token belongs there — Bearer is added automatically.
- Wire optional urlHelperText / secretHelperText through
GeneralSettingsConfig so other monitor types are unaffected.
- Add three new i18n keys (captureUrl.label/placeholder/helperText)
and one (secret.helperText) to all 16 locales. Non-English locales
carry the English copy as a placeholder until translators pick the
keys up via the existing PoEditor pipeline.
The transport JSON preview is strictly for verifying what is handed to
nodemailer.createTransport. The From header is built per-message in
sendMail, so showing it inside the transport JSON was misleading. The
separate From: ... pre block below the JSON is the right place for
that preview.
Brings back the second pre-block I dropped when consolidating the
preview. The transport JSON shows what's handed to nodemailer
createTransport; the From-header block shows the rendered envelope
recipients will see — they answer different questions and both belong.
Conditioned on systemEmailAddress so empty configs don't render an
empty pre.
Closes#3600. Previously the From header on test emails and notifications
showed only the raw email address. Adds an optional "Sender name" field
so emails appear from a friendly name like "Checkmate notifications"
<user@example.org>.
## Behaviour
- Field is optional. When empty/null/whitespace, the From header falls
back to the raw email address — identical to current behaviour, no
regression for existing installs.
- When set, the email service hands nodemailer { name, address } as the
message from field. Nodemailer handles RFC-5322 encoding (quotes,
commas, non-ASCII) so the display name is safe for any input.
- Unsetting works: clearing the input transforms the value to null in
the Zod parse step, and the settings repository already turns null
values into a $unset operation.
- Both client and server Zod schemas trim and empty-string-to-null,
preventing whitespace-only display names from being stored.
## Surfaces
- AppSettings schema, Settings type, EmailTransportConfig pick, server
Zod validation, client Zod validation, client form defaults, and the
Settings page form all carry the new systemEmailDisplayName field.
- The transport preview pre-block on /settings now includes a `from`
key showing exactly what the email service hands nodemailer — the
string when no display name is set, or { name, address } when it is.
- en.json carries the new label and placeholder. Other locales fall
back until translated (16 locales pending — separate follow-up).
## Conventions conformance (per docs/frontend-conventions.md)
- The pre-block I touched had three rule #1 violations (sx p,
borderRadius, backgroundColor); hoisted those to native props using
theme.shape.borderRadius. Other rule #1 violations elsewhere on this
page are pre-existing and intentionally left alone.
- No new sx-with-native-equivalents introduced.
- No color="text.X" shorthand introduced.
- No theme module import introduced.
The remove-all-monitors confirmation dialog only had a one-line title
and an empty body, which looked awkward and didn't communicate what was
actually about to be deleted alongside the monitors themselves.
- Adds a paragraph explaining that check history, incidents, scheduled
maintenance windows, and dependent status pages will also be removed,
with an inline tip to export monitors first to keep a backup.
- The Confirm button now uses confirmColor='error' to make the
destructive action visually distinct, with a localized
'Remove monitors' label.
- Translated to all 16 locales, with terminology aligned to each
locale's existing usage (e.g. zh-CN/zh-TW use 监控/監控 to match the
existing button label, vi uses 'trình giám sát', etc.).
When Capture runs inside a container (typical for Docker Desktop), the
only visible interfaces are loopback, so every bytes_sent/bytes_recv
counter is zero and the charts render as flat lines without any
explanation. Users couldn't tell whether Capture was unreachable, the
monitor was broken, or there was just genuinely no traffic.
Detect the case where checks exist (chart configs were built) but every
interface across every check reports zero bytesSentPerSecond and
deltaBytesRecv, and render a NoticeBanner above the chart grid
explaining it and suggesting Capture be run on the host.
Adds pages.infrastructure.charts.labels.netNoTraffic to all 16 locales.
- All uppercase headers (Active Incidents, Latest Incidents, Incident
Statistics, Resolved Incidents, summary card titles, details card
sections) now use the existing 'eyebrow' typography variant. That's
the same style used on the Logs page and lives in Theme.ts so future
tweaks land in one place.
- Latest Incidents and Incident Statistics rows now share a minHeight
so both cards line up vertically; Latest Incidents items dropped
their extra dividers and Box wrappers.
- Body text in the summary cards (and the details dialog) uses
variant=body1 (13px) consistently — previously a mix of body1/body2
and unspecified Typography.
- All lucide icons in CardSummary go through the design-system <Icon>
wrapper so they share the project's strokeWidth=1.5 default rather
than lucide's default 2.
- ControlsIncidentFilter: replaced the missing
pages.incidents.filters.clearFilters key (which was rendering as
raw text) with the existing common.buttons.clearFilters.
- CardDetails: extracted local SectionHeading and Cell helpers; switched
the cards' bare <Typography> cells to variant=body1 + variant=eyebrow
for headings; dropped redundant title-dividers (eyebrow style already
separates visually).
- AddTeamMemberDialog: First name / Last name now share a row, and
Password / Confirm password share a row. Each field uses sx={flex:1}
so the row totals match the width of the email/role rows below.
- InviteTeamMemberDialog: drop the redundant mt={theme.spacing(...)}
workaround on the form Stack (the new Dialog component provides this
spacing).
- Constants conformance: spacing literals replaced with LAYOUT.XS.
- Button.tsx: contained variant's hover now uses HOVER.DARKEN=0.06
instead of MUI's default 16%, which was visibly heavy on the green
primary buttons across the app. Also fixes a subtle bug where the
earlier hover sx merge order silently dropped the boxShadow:'none'
reset. Added a HOVER token group to constants.ts so the darken value
has a single source of truth.
- TextInput.tsx: forward the caller's sx to the wrapper Stack when a
fieldLabel is present (previously it was applied to the inner
TextField, so flex:1 etc. didn't propagate to the visible width).
Inner sx now also explicitly width:100% so wrapped inputs don't
collapse to content width when their parent is a flex row.
- FieldLabel.tsx: drop the hardcoded fontSize:'14px' and use
variant=body1 (=13px from theme) to match the rest of the app's
body typography.
Dialog component:
- Title bar gets a subtle background tint + bottom border. Title is
variant=h2 weight=600. Description tucks under the title with
variant=body1 secondary color (replaces MUI's DialogContentText).
- Action bar gets a matching subtle tint + top border. Cancel is now
outlined (was contained-error, which made every modal's Cancel button
look destructive). Confirm keeps its color (primary by default,
caller can pass error for destructive flows).
- Content area sits between with its own breathing room baked in, so
callers no longer need mt={theme.spacing(...)} workarounds.
- The cancelColor prop is removed; sites that genuinely need a
destructive Cancel can use confirmColor='error' on the Confirm button
instead.
NoticeBanner design-element:
- Extracted from the inline alert pattern in PageSpeedKeyPriorityFallback
into a reusable component with severity=info|warning|error.
- Refactored PageSpeedKeyPriorityFallback to use it.
DialogResolution.tsx: drop the now-removed cancelColor prop and the
mt-Box wrapper that's no longer needed (Dialog provides the spacing).
Fills in machine translations for ar, cs, fi, fr, ja, pt-BR, tr, uk, vi
so every locale now ships a native translation of the game-monitor
description (including the new server-query setup sentence and the
<gamedigLink> placeholder).
The game monitor type relies on each game's native query protocol — many
require a config file change on the game server (e.g. Minecraft Java
needs `enable-query=true` in `server.properties`). Without that, the
monitor silently fails to reach the server.
Append a sentence to the game-type general settings description that:
- tells the user some games need server-side queries enabled
- points them to the GameDig supported-games list for protocol details
Implementation:
- Use <Trans> with a `gamedigLink` slot so the link renders inline
- Loosen ConfigBox `subtitle` from `string` to `React.ReactNode` so the
subtitle can host the <Trans> output
- Update all 16 locale files. Six (de, es, ru, th, zh-CN, zh-TW) get a
full translation of the new sentence; nine empty ones (ar, cs, fi, fr,
ja, pt-BR, tr, uk, vi) fall back to the English string per the locale
gap policy
Four small follow-ups from a code review pass:
1. Drop the redundant `?? THEMED_RENDERERS[DEFAULT_STATUS_PAGE_THEME]`
fallback in Status/index.tsx — the map is now a total Record, so the
first lookup cannot return undefined.
2. Extract the literal "/status/public" into PUBLIC_STATUS_PAGE_PREFIX in
Types/StatusPage.ts and reuse from the three runtime call sites
(Status/index.tsx, HeaderStatusPageControls.tsx, StatusPagesTable.tsx).
The Routes/index.tsx route definition keeps its string literal — the
route table is otherwise all literals and the prefix only matters when
it's interpolated.
3. Delete two narrating comments above the public/admin branches in
Status/index.tsx — the JSX is self-describing.
4. Move the inline `style` object construction in StatusPageThemeProvider
into the existing useMemo so the wrapper div receives a stable style
reference. The previous spread defeated the surrounding memoization on
every render.
The 4 themed renderers (refined, modern, bold, editorial) cover every
StatusPageTheme value and the model defaults to "refined", so the legacy
fallback path in Status/index.tsx was unreachable in practice.
Removing it lets us delete:
- theme-overrides.css (the only remaining CSS file in StatusPage)
- StatusBar / MonitorsList / InfrastructureMetrics (only the legacy
fallback consumed them)
- The .sp-* className hooks that wired CSS rules into the components
Pure sx via the per-theme styles.ts recipes is now the single source of
truth for every status page surface. Net diff is -801 / +42.
Status/index.tsx now resolves a renderer from the theme map (falling back
to the default theme) and keeps the public-route and admin-preview
branches it already had.
Second /simplify pass picked up:
- pillHardware in refined and modern hardcoded blue hex codes (#60a5fa, #1d4ed8, #2563eb) while bold and editorial already used the green token system. Switch refined and modern to tokens.up / tokens.upSoft so all four themes are consistent and the public status page no longer leaks blue
- pill and pillHardware in refined and modern duplicated the shared 6 typography fields (fontSize, textTransform, letterSpacing, padding, borderRadius, fontWeight). Extract a pillBase constant per theme and spread, leaving only the colour deltas on each
- The 4 themes each defined their own font-stack constants. Consolidate them in themes/shared/fontStacks.ts (SANS_STACK, BOLD_SANS_STACK, SERIF_STACK, EDITORIAL_SECONDARY_SANS_STACK, MONO_STACK) and import. Drops 6 duplicated literal stacks
- modern dark-mode card hover used the light-mode shadow as a fallback. Add darkShadowHover and pick it up via cardHoverShadow when isDark
- refined and editorial styles factories silence their unused isDark parameter with `void isDark` instead of an underscore prefix; bold and modern still use it actively
Apply review feedback:
- Editorial chartSwitch: drop the per-button isFirst flag and put the divider on the parent via "& > button + button { borderLeft }"; chartSwitchButton(active) now matches bold/modern's signature
- Drop the empty company: {} entries in bold/modern and the wrapper span around the company name; the parent brand row already styles it
- Editorial styles factory: keep the (tokens, isDark) signature for cross-theme uniformity but acknowledge the unused isDark with a void-statement instead of an underscore prefix
Finishes the migration started for refined. Each theme now consumes the same pattern:
- A typed *Styles interface that names every entry the page consumes (no `as Function` casts at the call site)
- A *Styles factory that takes (tokens, isDark) and returns sx recipes derived from existing tokens.ts plus the shared toneColor/toneSoft helpers in shared/overallStatus
- A useMemo around the factory call so MUI's sx processor sees referential equality across renders that don't change tokens
- modern.css's keyframes (pulse + staggered fade-in + reduced-motion guard) ported via @mui/system keyframes inside the modern styles factory
- bold.css's hero glow + conic logo + pseudo-elements ported as inline sx via "&::before" / "& > *" selectors
- editorial.css's serif typography + hairline dividers + flat rectangular badges ported as plain sx with the serif/sans family stacks named at module scope
Drop the four .css files (~1789 lines) along with the className= attributes and the import "./*.css" lines in each StatusPage component.
The shared themed components (ThemedHeatmap, ThemedHistogram, ThemedInfrastructure) lose their legacy classPrefix branch entirely now that no theme uses it. The dual-API gate is gone; there is one path.
Verified: tsc --noEmit clean, prettier clean.
Note: theme-overrides.css remains for the legacy non-themed status page components (StatusBar, MonitorsList, HeaderStatusPageControls). That's separate scope from the four-theme work the reviewer flagged; can be migrated in a follow-up.