Reorder controls stayed visible after a click. The reveal used
`:focus-within`, and clicking a button leaves it focused, so that card's
controls stayed pinned open long after the pointer left — Failed checks
being wherever focus happened to land. Now `:has(:focus-visible)`, which
matches keyboard focus only, so a mouse click no longer pins them.
Widths: Currently down was full-width and Recent incidents 8-wide, both
with more whitespace than content. Currently down carries six columns per
row so it is the densest card, but it does not need all twelve; it is now
8. Recent incidents carries four and is now 4, as are Recent errors.
Declaring a narrower width is not enough on its own: a card left alone in
a row gets stretched to fill it, so Currently down at 8 still rendered at
12 while it was the only card in its row. Registry and default order are
now sequenced so widths pair into full rows — 8 beside 4, 6 beside 6 —
which is what actually makes the narrow cards render narrow. Verified
across the full catalogue, the non-admin subset and the defaults: every
mid-page row fills exactly 12 with no gaps.
Fonts: every secondary line used typographyLevels.s, which is 11px —
body2, a de-emphasis size. The theme's body1 is 13px, so all 24 uses are
now typographyLevels.m. Nothing in a card renders below body size any
more; only the large figures differ, at xxl.
Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
Adds move-up / move-down controls to each card, revealed on hover or
when something inside the card takes focus, so a settled dashboard stays
quiet but the controls are still reachable by keyboard.
The controls reach the card header through context rather than a prop,
so none of the 17 card components has to know its own position — the
grid owns ordering. Positions resolve against the *visible* order, not
the stored array: a non-admin never sees the admin cards, so moving a
card past one of them would otherwise appear to do nothing. Hidden cards
keep their stored positions.
Styling fixes:
- 'Reset to defaults' no longer renders as the prominent green contained
button. It is a destructive secondary action, so it moves to the quiet
outlined slot beside the close action. The dialog now has no confirm
button at all, since every change applies immediately and there is
nothing to confirm.
- Icons go through the shared Icon component at the codebase's usual 16px
instead of ad-hoc 14px with a one-off stroke width, and the header
spinner matches at 16px.
- Named the remaining magic numbers — header height, bar thickness,
sparkline and column widths, status tile width, control sizes — so they
read as decisions rather than arbitrary pixels. There is no size-token
scale in the theme (LAYOUT is spacing only) and peer components
hardcode their own dimensions, so module constants match the house
pattern rather than inventing a scale.
- Centralised the two poll rates instead of repeating 30000 and 60000
across cards, and hoisted a stray sx flexShrink onto the component.
Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
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