Skip to content

Redesign the dashboard welcome page's active alerts #2046

Description

@SeanWhelan

Problem

The landing page renders AlertingOverview once per entity type, from inside StatOverview. Concretely, in a tenant with active alerts you get one overflowing alert list under Sources plus two equally-weighted "No Active Alerts" placeholders under Collections and Destinations.

Specific defects, all visible on a real tenant today:

  1. Alerts are partitioned by entity type. EntityStatOverview renders three StatOverviews, each of which renders its own AlertingOverview (src/components/home/dashboard/EntityStatOverview/StatOverview.tsx). Urgency has nothing to do with whether a task is a capture or a materialization, so the grouping splits related problems apart and spends equal vertical space on two empty states.
  2. Nested cards. AlertingOverview renders a CardWrapper inside the tinted StatOverview container.
  3. Three concurrent queries for one dataset. Each AlertingOverview instance runs the same alerts(by: { prefix, active: true }) query and then filters client-side on alertDetails.spec_type. Three round trips, three caches, one dataset.
  4. Catalog names truncate at the wrong end. acmeCo/foo/source-post… clips the identifying tail and keeps the redundant prefix.
  5. No urgency encoding. shard_failed (task is down) and free_trial_ending render identically as ChipList chips. firedAt exists only as a tooltip, and there is no sort.
  6. Unbounded growth. The list has no cap, so alert count drives page height.
  7. Alert type is the whole payload, rendered as a chip — the lowest-information component available for the most important field.

Proposed change

Replace the three per-entity alert lists with one alerts panel below the stat cards.

  • Single alerts(by: { prefix, active: true }) query for the panel.
  • Rows read left to right: tier → alert type → task identity → relative time.
  • Sorted by tier, then firedAt.
  • Entity type shown as the same iconoir glyph the stat cards use, sourced from arguments.spec_type (already present in the query response).
  • Task identity clips at the head so the identifying tail always survives, with the prefix path as dimmed secondary text.
  • 3+ tasks sharing one alert type collapse into a single expandable row.
  • Stat cards go back to being only stats. Their existing hooks are untouched.
  • Empty state is a single line, not a bordered placeholder card per entity.

Tier derivation

There is no severity field in the APIAlertTypeInfo exposes alertType, description, displayName, isDefault, isSystem, and nothing ranks urgency. The docs assign no severity either.

What is defined is an escalation order, documented for task failures: shard_failedtask_chronically_failingtask_auto_disabled_failing. The default thresholds show the distance: shardFailed fires at 3 failures in 8h, while taskChronicallyFailing.condition.failingFor defaults to 30d.

Proposed client-side tiers, following that documented order:

Tier Rule AlertType values, worst first
Failing Terminal or advanced escalation stage; task stopped or long-erroring task_auto_disabled_failing, task_auto_disabled_idle, task_chronically_failing, shard_failed, background_publication_failed
Degraded First escalation stage, or no-data with the task still alive data_movement_stalled, auto_discover_failed, task_idle
Billing The docs' own Billing Alerts category; tenant-scoped, not a data fault missing_payment_method, free_trial_stalled, free_trial_ending, free_trial

This mapping needs sign-off from someone who triages these daily. It is a UI-side convention, not read from the API. Known soft spots:

  • data_movement_stalled is only meaningful relative to its configured interval, so a 30-day-interval task firing it is not comparable to a 2-hour one. A flat tier cannot express that.
  • auto_discover_failed can atomically block unrelated schema fixes, which makes it worse than its tier suggests.

Also in scope

  • Source display strings from the API. Replace the hardcoded humanReadableIntlKey map in src/settings/alerts.ts with displayName / description from the existing alertTypes query. AlertTypeProvider and useGetAlertTypes (src/context/AlertType.tsx) already fetch this. This removes a recurring bug class: background_publication_failed shipped rendering blank because a new alert type has to be registered in two separate places and one was missed. lang/ is en-US only, so there is no translation coverage to lose.
  • Rename the page to "Overview." One string: 'routeTitle.home' in src/lang/en-US/RouteTitles.ts, currently Welcome. Everything else already says home (pages/Home.tsx, lang/en-US/HomePage.ts, all home.* keys). "Welcome" greets a first-time visitor, which is not who this page is for. Note details.tabs.overview already exists for entity detail tabs; the two never appear side by side, but don't add a tenant-scoped Overview component next to EntityStatOverview without a clearer name.
  • Panel header "Active Alerts." The existing alerts.overview.title.active string is Active Alerts Summary; drop "Summary" as filler.
  • Any "N tasks affected" count must exclude the org-scoped billing types, or it overcounts by one whenever a billing alert is live.

Explicitly out of scope

  • No resolve / dismiss / acknowledge affordance. No such mutation exists, and this is by design: alerts are derived state, with resolvedAt stamped by the system when the condition clears. The four alert mutations (createAlertSubscription, updateAlertSubscription, deleteAlertSubscription, updateAlertConfig) all target subscriptions and configs, never an alert instance. A dismiss button that only hides locally would lie; one wired to updateAlertConfig .enabled = false would silently disable an alert type for the whole prefix.
  • Stat card data sources stay on PostgREST. useActiveEntityCount and useMonthlyUsage are left exactly as they are. No new PostgREST is introduced.
  • totalCount on ref connections — needs backend, tracked separately (see below).
  • A server-side severity field on AlertTypeInfo — would make the tier mapping authoritative rather than a UI convention, but is not required for this work.

Data and API

Everything the redesign needs already exists in GraphQL. No backend work.

Need Source New?
Alert rows (alertType, firedAt, catalogName, arguments, resolvedAt) alerts(by: { prefix, active }) Already used
Entity type per row arguments.spec_type Already used for filtering
Display names / descriptions alertTypesAlertTypeInfo Query + provider already exist
Row link targets catalogName + useDetailsNavigator (pure URL building, no fetch) Existing
Tiers, grouping, sort, counts derived client-side No fetch
Optional tenant-level all-alerts page same alerts(by: { prefix }) with active omitted; AlertsBy.active is optional and documented to return both active and resolved. Cursor pagination via after/before/first/last on AlertConnection No backend

Where the links go

  • Task names → that task's existing Alerts tab (/captures/details/alerts and the collection/materialization equivalents). The one alert destination that exists today.
  • Billing rows → /admin/billing.
  • A tenant-level "all alerts" link has no valid target today. There is no tenant-level alerts route in app/routes.ts, and components/tables/AlertHistory queries through liveSpecs(by: { names }) so it can only ever show one task. Two lang strings are dead with zero references — alerts.overview.title.active.all ("All Active Alerts") and alerts.overview.listTruncated.cta ("See all active alerts") — and AlertTruncationMessage sets justifyContent: 'space-between' on a Stack with a single child. This CTA was designed and never wired. Either build the route in this work or omit the link. A capped list without a real overflow target just hides alerts.

Accessibility

  • Tier glyphs are uniform circles differentiated by colour only. This is acceptable because every glyph has an adjacent text label ("FAILING", "1 failing"), so colour is not the sole carrier and WCAG 1.4.1 holds. Do not use a bare glyph without its label.
  • Light mode caveat, measured: the Failing and Degraded dot colours sit at ~1.05:1 against each other (effectively identical luminance) with only a ~57° hue gap, which is the weak axis for red-green CVD. Labels preserve correctness; at-a-glance scanning degrades. Unavoidable within colour-only, since amber cannot be both recognisably amber and 3:1 on white. Filled-vs-hollow circle is the escape hatch if wanted.
  • The pulse on Failing rows animates transform and opacity only, never layout, via a pseudo-element. Must be wrapped in prefers-reduced-motion: reduce.
  • The grouped/expandable row needs an accessible name, aria-expanded, aria-controls, and keyboard activation. Easy to miss: making a <li> role="button" without a label produces an unnamed button.
  • Contrast must be re-verified against the real MUI theme values. The numbers from the prototype were measured on prototype-local tokens, not on context/Theme.tsx exports. Do not assume the shipped values pass or fail; measure with alpha compositing (naive getComputedStyle comparisons ignore rgba compositing and give wrong ratios). If a shared theme token does need changing, note the blast radius across other pages.

Open decisions

  1. Sort within a tier: firedAt newest first, or oldest first? Oldest-first argues that a task broken for a month is doing more damage than one broken for two hours.
  2. Is the tenant-level /alerts route in scope? Decides whether the overflow link ships or is omitted.
  3. Row cap: what N, and does the panel need first: on the query? The current query passes no limit at all, which is a latent problem for tenants with many active alerts independent of this redesign.
  4. Sign-off on the tier mapping above.

Suggested PR split

  1. Restructure. Lift AlertingOverview out of StatOverview into a single panel; collapse three queries into one; stat cards become stats only. No visual tier work yet.
  2. Tier derivation, grouping, sort, row layout, glyphs + pulse. The presentation layer.
  3. Wire alertTypes for display strings, retiring the humanReadableIntlKey map.
  4. Naming: routeTitle.home → "Overview", panel header → "Active Alerts".
  5. (Optional, gated on decision 2) Tenant-level /alerts route.

Related follow-ups (separate issues, backend)

  • totalCount on LiveSpecRefConnection. Not a blocker here, but a live correctness bug: RESPONSE_DATA_LIMIT = 1000 and useActiveEntityCount sets indeterminate: data.length >= RESPONSE_DATA_LIMIT, which is what renders the 1351+ on the stat card. The count is capped at the fetch limit and the true value is not shown. A server-side count fixes the wrong number and would also let the stat cards move off PostgREST.
  • severity on AlertTypeInfo, to make tier ranking authoritative instead of a duplicated UI convention.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions