Completes the catalogue at 17 cards and adds the picker that chooses
between them.
New cards: Slowest monitors, Lowest uptime, Monitors by group, Busiest
servers, Incident stats, Notification channels, Maintenance, Status
pages, Checks on schedule, Recent errors, Team.
Data caveats honoured from the specs:
- Rankings exclude paused monitors. A paused ping monitor reads 0% uptime
and would otherwise rank beside a genuinely down one; neither is a live
problem. Rankings also hide below two entries — a ranking of one is not
a ranking.
- Busiest servers reads cpu/memory/disk from the newest recentChecks
entry (snake_case, 0-1 fractions) and renders a dash for a down server
rather than 0%.
- Maintenance compares start/end client-side: `active` is a user on/off
flag, not 'running now', and repeat > 0 means end may sit in the past
for a live window.
- Notification channels renders only the type. The response also carries
accessToken and accountSid; those must never reach the DOM.
- Recent errors is worded 'recent' because /logs is a 1000-entry
in-memory ring buffer that rotates fast and is wiped on restart.
Also fixes a layout gap the six default cards never exposed: a 6-wide
card followed by an 8-wide one left a lone card stretched to 8 in a
middle row, with 4 empty columns beside it. Mid-page rows now always fill
to 12; only the final row may end short, and the cap applies only there,
so a lone trailing 4-wide card does not become a banner.
Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
recentChecks is oldest-first, not newest-first. The server appends each
snapshot with `$push … $slice: -N` (monitor.repository.mongo.ts:218) and
returns it unreversed, and every existing consumer reads
recentChecks[length - 1] as the latest. I had it backwards, which broke
three things:
- downSince() walked from index 0 and broke on the first element. For any
monitor that was up before it failed — the normal case — the very first
check is an "up", so the loop exited immediately and the downtime
duration never rendered at all.
- The sparkline reversed already-ascending data, drawing every trend line
mirrored: climbing latency appeared to be improving.
- The status code read index 0, the oldest retained check, so a monitor
that served 200s before failing showed a green 200 beside a red dot.
Also from review:
- Drop `initializing` from the "Currently down" ranking. On a fresh
install every new monitor starts there, so the card opened red claiming
a fleet-wide outage. It means "not checked yet", not "broken".
- Add the maintenance count to Monitor status. The summary reports it and
the server counts it; omitting it left the tiles summing to less than
totalMonitors with nothing explaining the gap.
- Link the status tiles to plain /uptime. The monitors list never reads
query params, so ?status= was silently dropped and the link lied.
- Render the status code as plain text instead of StatusCodeLabel, whose
Tooltip put a focusable element inside the row's link where no keyboard
user could reach it.
- Stop sending `limit`: findByTeamIdWithStats derives its page size from
rowsPerPage alone and never reads it, so it only implied a truncation
that was not in effect.
- Persist a deliberately emptied dashboard instead of resetting it to the
defaults on reload.
- Pass isStale on every card. The prop and its dimming were implemented
but never wired, so background refetches gave no feedback.
Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
Adds /dashboard, a cross-type overview answering "is anything wrong, and
where" — the question no single-type page can answer today. /uptime shows
uptime monitors, /infrastructure shows hardware, /incidents shows history;
nothing joined them.
Layout is a 12-column grid. Each card type declares a minimum width (4, 6,
8 or 12) in the registry; the grid stretches the last card of a short row
to fill the remainder, capped at 8 so one card never balloons across a
near-empty row. Declared width is never a user setting. grid-auto-flow:
dense was tried and does not help — no card is small enough to backfill
the gaps that occur in practice.
Six cards ship as the default set:
Monitor status the five fleet counts, server-computed
Currently down the centrepiece: everything wrong now, worst first
Failed checks failure rate over the last day
Recent incidents latest five, with the open count
Monitors by type composition, flagging types with something down
Checkmate server the only card that works with zero monitors (admin)
Cards 1-3 and 5 ride a single /monitors/team/with-checks call shared
through context, so adding a monitor-derived card costs no extra request.
Notes on the data:
- Ordering in "Currently down" is down -> breached -> initializing, tied
on longest downtime. That is ordering derived from existing fields, not
a new severity scale — Checkmate has none, and inventing one would be a
product decision dressed as a design choice.
- The healthy state is deliberately quiet: one line, not a wall of green.
- Failed checks ships the day range only. dateRange=week returns figures
identical to day, so a range selector would silently lie.
- Admin-only cards are hidden rather than disabled, and are filtered out
of the selection entirely for non-admins.
Card selection persists to localStorage, validated on read so a card
removed in a later release cannot break the page. The picker and the
remaining catalogue cards are not built yet.
The Containers card from the spec is deliberately absent: it needs
containerSummary on the check snapshot, which arrives with the Docker
monitoring work. cards.ts records where it slots in.
Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
Requested by a user who found xx.xx% more trustworthy than xx.x% on their
status page. The presentational argument holds, but the stronger reason is
that one decimal was actively misleading.
Intl rounds to the displayed precision, so every value above 99.95% rendered
as "100.0%" — a claim of zero downtime for a period that contained real
outages. On a public status page that is the opposite of reassuring: the
number reads as flawless during a month with a visible incident.
One decimal also collapsed the range operators care about most. 99.9%,
99.95% and 99.99% are three very different commitments (8.7 hours, 4.4 hours
and 52 minutes of annual downtime) and the first rendered as 99.9% while the
other two were indistinguishable at 100.0%.
Everything flows through the shared formatter in FormatUtils, so this covers
both surfaces at once: the monitor list and diagnostics gauges in the web
app, and the uptime figure, chart cells and hover tooltip on status pages.
Infrastructure gauges are untouched — they format separately at zero
decimals, where CPU at "43.00%" would only add noise.
Server-side precision was already sufficient; the aggregation divides
without rounding, so no API change is needed.
Adds tests pinning the regression: 7 of the 10 fail against the previous
one-decimal formatter.
Note: this restores the client Vitest setup. PR #3844 introduced it, but the
config, scripts and logger tests were dropped when that branch merged into
develop, leaving the client with no test runner again.
The monitor picker on the status page create/configure form fetched
candidates using a hardcoded list of monitor types that omitted "dns", so
DNS monitors were filtered out server-side and never appeared in the
dialog. Everything else was already in place: the DNS provider stores
checks like any other monitor type, and the status page renders them once
attached.
Rather than appending "dns" to the hardcoded list, derive the query from
the existing SelectableMonitorTypes tuple in @/Types/Monitor. The two lists
had silently drifted apart, and deriving prevents the next new monitor type
from hitting the same bug. "hardware" is appended explicitly because
infrastructure monitors are created on their own page and so are absent
from the selectable set.
Net effect on the requested types: gains "dns", loses none.
Fixes#3721
Adds a regression suite for #3843 so the leak cannot return silently. The
client had no test runner, so this introduces Vitest, which reuses the
existing Vite resolution and the `@/` alias rather than needing a separate
transform pipeline. jsdom is required because the logger reads
window.location and navigator.userAgent.
The tests drive the exported `logger` rather than the private redaction
helpers, so they cover the wiring as well as the logic. That distinction
matters here: the first attempt at this fix redacted the JSON branch but
left the debug branch printing raw objects, a gap that testing the helper
in isolation would not have caught. Each case runs at both error and debug
level for the same reason.
Covered: the reported login payload, credentials carried on an AxiosError
(config.data and the Authorization header), non-JSON request bodies,
passwords on class instances, key-matching variants, and circular payloads.
One asserts non-sensitive context survives, so redaction cannot regress
into blanket suppression.
Verified by reverting the fix: 16 of 23 tests fail against the original
behaviour, including a pre-existing crash on circular payloads.
Follow-up to the initial redaction pass, addressing four defects:
Class instances bypassed redaction entirely. The prototype guard was meant
to skip FormData/Blob but caught every non-literal object, returning it by
reference — so a credential on a DTO or form-state instance was logged
verbatim, and the deep-copy contract was violated. The check is now an
explicit deny-list of opaque types; everything else is walked.
The raw Error was still logged by the debug branch. An AxiosError carries
config.data (the serialized request body) and config.headers.Authorization
(set for every request in ApiClient), so the password redacted from the
context was printed straight back out via the error object. Errors now go
through toSafeError(). Because Axios serializes the body to a string, which
key-based redaction cannot see into, it is parsed before redaction; a body
that is not JSON is replaced wholesale rather than logged raw.
The Blob guard was gated on `typeof FormData !== "undefined"`, so in an
environment with FormData but no Blob the instanceof check threw a
ReferenceError — from inside a catch block, turning a handled API error
into an unhandled crash. Each constructor is now resolved defensively.
Also widened the key list (cookie, jwt, privateKey, sessionId, csrf, pwd)
and dropped a redundant type annotation.
Verified with 18 assertions covering the reported payload, class instances,
cycles, shared siblings, axios error shapes, and a missing Blob global.
Failed requests logged the full request body to the browser console, so a
failing POST to /auth/login wrote the user's plaintext password where it
could be captured by error-reporting tools, screen shares, or support
sessions.
Redaction is applied centrally in the logger rather than at the call sites,
so every current and future caller is covered:
- createLogData() redacts context before it is serialized, covering the
JSON branches of error() and warn().
- The debug-level branches of error() and warn() printed the raw context
object directly, bypassing createLogData(); they now print the redacted
copy.
- debug() and info() redact their free-form data argument.
Keys are matched case-insensitively on a substring after stripping dashes
and underscores, so password, newPassword, X-Api-Key and Authorization are
all caught. The walk copies rather than mutates the caller's object, cuts
genuine cycles without mislabelling repeated siblings, and renders
FormData/Blob as a type marker instead of walking them.
Affected paths were POST (/auth/login, /auth/register) and PATCH/PUT, which
carry monitor secrets. Error-level logging is on by default, so this was
not limited to debug builds.
Fixes#3843