@checkstack/anomaly-frontend 0.6.13 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,134 @@
1
1
  # @checkstack/anomaly-frontend
2
2
 
3
+ ## 0.6.15
4
+
5
+ ### Patch Changes
6
+
7
+ - @checkstack/healthcheck-frontend@0.37.1
8
+
9
+ ## 0.6.14
10
+
11
+ ### Patch Changes
12
+
13
+ - 6c8b36b: The Logs, Metrics, and Traces cards on the system overview page now match the
14
+ other cards. They had drifted to a flat `bg-card` background with a
15
+ hairline-only shadow, so they rendered visibly flatter than their siblings
16
+ (health, dependency, SLO, incident, anomaly, maintenance), which all use the
17
+ detail-page gradient plus a soft two-layer elevation shadow.
18
+
19
+ The shared card surface is now a single primitive - `DetailCard` (and the
20
+ `detailCardSurface` / `detailCardSurfaceFlat` class constants) in
21
+ `@checkstack/ui` - instead of a className that was copy-pasted (and could
22
+ diverge) in every system-overview card. All of those cards now render from the
23
+ one primitive, so they cannot drift apart again. A new `error`-level ESLint
24
+ rule `checkstack/no-inline-detail-card-chrome` fails the build if a card in that
25
+ family re-declares the surface inline instead of using `DetailCard`.
26
+
27
+ - 6c8b36b: Smooth out loading states so surfaces no longer flash a wrong resolved state or
28
+ pop content in one piece at a time.
29
+
30
+ - **Dashboard no longer flashes "all systems healthy".** The overview aggregates
31
+ per-system signals from many plugins (health, incidents, SLOs, anomalies,
32
+ dependencies, log/metric/trace streams), each reporting asynchronously - so
33
+ before any had loaded, an empty problem list briefly read as an all-clear.
34
+ `SystemSignalsSlot` gains an additive `onLoadingChange` report; every source
35
+ filler reports its load state, and the dashboard holds its existing skeleton
36
+ until all mounted sources have settled (bounded by a grace period so a
37
+ non-reporting source cannot hang it).
38
+ - **System detail overview cards reveal together.** Each `SystemDetailsSlot` card
39
+ self-loads and several self-hide when empty, so they popped in one after
40
+ another. The slot gains an additive `onLoadingChange`; each card reports, and
41
+ the detail page keeps the cards mounted but behind a skeleton set until all
42
+ have settled, then reveals them at once - no stagger, no layout shift, and
43
+ cards with no content simply never appear.
44
+ - **Catalog manage "Health" column no longer pops in.** `CatalogBrowseHealthSlot`
45
+ gains an additive `onLoading` report (sourced from the health filler's bulk
46
+ fetch); the manage Systems tab shows a per-row placeholder until the health
47
+ data settles, so the status badges swap in instead of appearing onto an empty
48
+ cell. The same tab also keeps its state badges on one row (side by side)
49
+ instead of wrapping.
50
+ - The system detail **Dependencies** and **Logs / Metrics / Traces** cards are now
51
+ collapsed by default: each shows a compact "<title> N" summary and expands its
52
+ detail on click, so the overview column stays short. They render through a new
53
+ shared `CollapsibleDetailCard` (`@checkstack/ui`) that single-sources the header
54
+ layout (icon + title + count + rotating chevron) so every collapsible overview
55
+ card is vertically centred and behaves identically - the earlier per-card header
56
+ markup had drifted and left the Logs/Metrics/Traces titles off-centre when
57
+ collapsed.
58
+ - Moved the system detail **SLO card** from the full-width alert strip into the
59
+ left (monitoring) column, so it sits at the same width as the dependencies and
60
+ health cards; only maintenances and incidents stay full width. It now joins the
61
+ coordinated card reveal above.
62
+ - Removed a dead, unreferenced duplicate dashboard component
63
+ (`dashboard-frontend/src/Dashboard.tsx`); the live overview is
64
+ `DashboardSystemHealthSection`.
65
+
66
+ All slot-contract additions are optional/additive - existing fillers and
67
+ consumers keep working unchanged.
68
+
69
+ - 6c8b36b: Edit forms stay stable while you are typing. Previously, editing a system's
70
+ description (and many other edit dialogs/settings pages) would reset the field
71
+ mid-edit whenever a webhook update or realtime signal refetched the underlying
72
+ query: the form re-seeded its local state from the fresh query result on every
73
+ refetch. Forms now seed their local state ONCE - on the dialog's open
74
+ transition, or once per record via a stable key - and ignore background
75
+ refetches while you are editing.
76
+
77
+ New shared primitive `useSeedFormOnOpen(open, onInit)` in `@checkstack/ui`
78
+ (alongside the existing `useInitOnceForKey`) seeds a dialog form once per
79
+ open transition, StrictMode-safe. Fixed surfaces include the catalog
80
+ system/environment/group editors, the healthcheck platform-defaults dialog,
81
+ the SLO / gitops-provider / telemetry-source / satellite / announcement /
82
+ role edit dialogs, and the cache / queue / notification / secrets / anomaly /
83
+ profile / strategies settings pages (query-seeded pages also drop their loader
84
+ cache via `gcTime: 0` so a warm cache cannot race the one-shot seed).
85
+
86
+ - 6c8b36b: Speed up the catalog manage Systems tab and unify its per-row actions.
87
+
88
+ - The per-row `SystemHealthCheckAssignment` no longer runs two allocation-heavy
89
+ access hooks (`useCanAccessType` + `useResourceAccess`) plus a counts query
90
+ PER ROW - profiling showed this as the dominant, GC-bound cost of opening the
91
+ Systems tab. A new `CatalogSystemHealthCheckDataProvider`, folded around the
92
+ catalog tree via `CatalogBrowseDataBoundarySlot`, resolves the gate + counts
93
+ once for the whole visible list; the row action reads them from context (the
94
+ heavy standalone path is only rendered on surfaces without the provider, e.g.
95
+ the system detail page).
96
+ - The per-row `SystemAnomalyBadge` no longer instantiates two live query
97
+ observers (and scans up to 500-element arrays) per row. A new
98
+ `AnomalyBadgeDataProvider`, folded around the catalog browse/manage tree via
99
+ `CatalogBrowseDataBoundarySlot`, fetches the active + suspicious anomaly sets
100
+ once and exposes an O(1) per-system lookup - matching the SLO / incident /
101
+ health / dependency badges. Without the provider the badge falls back to its
102
+ own (deduped) queries, so the system detail page is unchanged.
103
+ - `ScopeSystemToTeamAction` and `SystemHealthCheckAssignment` now render through
104
+ the shared `RowAction`, so a system row's action cluster looks uniform.
105
+ `ScopeSystemToTeamAction` additionally defers mounting its Radix dialog until
106
+ first use, so a table of rows no longer mounts an idle dialog per row.
107
+ - `@checkstack/ui` `RowAction` gains an optional `badge` (e.g. an assigned-count
108
+ indicator) rendered next to the icon, so a count action stays a normal
109
+ `RowAction` instead of a bespoke button.
110
+
111
+ - Updated dependencies [6c8b36b]
112
+ - Updated dependencies [6c8b36b]
113
+ - Updated dependencies [6c8b36b]
114
+ - Updated dependencies [6c8b36b]
115
+ - Updated dependencies [6c8b36b]
116
+ - Updated dependencies [6c8b36b]
117
+ - Updated dependencies [6c8b36b]
118
+ - Updated dependencies [6c8b36b]
119
+ - Updated dependencies [6c8b36b]
120
+ - Updated dependencies [6c8b36b]
121
+ - @checkstack/ui@1.29.0
122
+ - @checkstack/healthcheck-frontend@0.37.0
123
+ - @checkstack/healthcheck-common@1.18.0
124
+ - @checkstack/catalog-common@2.8.0
125
+ - @checkstack/frontend-api@0.16.1
126
+ - @checkstack/notification-frontend@0.9.6
127
+ - @checkstack/common@0.23.0
128
+ - @checkstack/anomaly-common@1.8.2
129
+ - @checkstack/notification-common@1.7.2
130
+ - @checkstack/signal-frontend@0.3.7
131
+
3
132
  ## 0.6.13
4
133
 
5
134
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/anomaly-frontend",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -16,16 +16,16 @@
16
16
  "lint:code": "eslint . --max-warnings 0"
17
17
  },
18
18
  "dependencies": {
19
- "@checkstack/anomaly-common": "1.8.1",
20
- "@checkstack/catalog-common": "2.7.3",
21
- "@checkstack/common": "0.22.0",
22
- "@checkstack/frontend-api": "0.16.0",
23
- "@checkstack/healthcheck-common": "1.17.0",
24
- "@checkstack/healthcheck-frontend": "0.36.2",
25
- "@checkstack/notification-common": "1.7.1",
26
- "@checkstack/notification-frontend": "0.9.5",
27
- "@checkstack/signal-frontend": "0.3.6",
28
- "@checkstack/ui": "1.28.2",
19
+ "@checkstack/anomaly-common": "1.8.2",
20
+ "@checkstack/catalog-common": "2.8.0",
21
+ "@checkstack/common": "0.23.0",
22
+ "@checkstack/frontend-api": "0.16.1",
23
+ "@checkstack/healthcheck-common": "1.18.0",
24
+ "@checkstack/healthcheck-frontend": "0.37.1",
25
+ "@checkstack/notification-common": "1.7.2",
26
+ "@checkstack/notification-frontend": "0.9.6",
27
+ "@checkstack/signal-frontend": "0.3.7",
28
+ "@checkstack/ui": "1.29.0",
29
29
  "date-fns": "^4.4.0",
30
30
  "lucide-react": "^1.17.0",
31
31
  "react": "19.2.7",
@@ -33,7 +33,7 @@
33
33
  "zod": "^4.2.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@checkstack/scripts": "0.7.5",
36
+ "@checkstack/scripts": "0.7.6",
37
37
  "@checkstack/tsconfig": "0.0.7",
38
38
  "@types/react": "^19.0.0",
39
39
  "typescript": "^5.0.0"
@@ -0,0 +1,69 @@
1
+ import React, { createContext, useContext, useMemo } from "react";
2
+ import { usePluginClient } from "@checkstack/frontend-api";
3
+ import { AnomalyApi } from "@checkstack/anomaly-common";
4
+
5
+ interface AnomalyBadgeDataContextValue {
6
+ /**
7
+ * The worst anomaly state for a system from the two bulk fetches, or
8
+ * `undefined` when the system has no active/suspicious anomaly.
9
+ */
10
+ getSystemState: (systemId: string) => "anomaly" | "suspicious" | undefined;
11
+ loading: boolean;
12
+ }
13
+
14
+ const AnomalyBadgeDataContext = createContext<
15
+ AnomalyBadgeDataContextValue | undefined
16
+ >(undefined);
17
+
18
+ /**
19
+ * Provider that fetches the active + suspicious anomaly sets ONCE and derives an
20
+ * O(1) per-system lookup, so per-row `SystemAnomalyBadge`s read from context
21
+ * instead of each instantiating its own live query observers and scanning the
22
+ * (up to 500-element) arrays. Without this provider each badge falls back to its
23
+ * own (deduped) queries, so surfaces that don't install it still work.
24
+ *
25
+ * The `getAnomalies` queries are unscoped (they return every active/suspicious
26
+ * anomaly, capped at 500), so this provider ignores `systemIds` for fetching and
27
+ * only uses the results to build the lookup once.
28
+ */
29
+ export const AnomalyBadgeDataProvider: React.FC<{
30
+ children: React.ReactNode;
31
+ }> = ({ children }) => {
32
+ const anomalyClient = usePluginClient(AnomalyApi);
33
+
34
+ const { data: confirmed = [], isLoading: confirmedLoading } =
35
+ anomalyClient.getAnomalies.useQuery(
36
+ { state: "anomaly", limit: 500 },
37
+ { staleTime: 30_000 },
38
+ );
39
+ const { data: suspicious = [], isLoading: suspiciousLoading } =
40
+ anomalyClient.getAnomalies.useQuery(
41
+ { state: "suspicious", limit: 500 },
42
+ { staleTime: 30_000 },
43
+ );
44
+
45
+ const contextValue = useMemo<AnomalyBadgeDataContextValue>(() => {
46
+ const confirmedIds = new Set(confirmed.map((a) => a.systemId));
47
+ const suspiciousIds = new Set(suspicious.map((a) => a.systemId));
48
+ return {
49
+ getSystemState: (systemId) => {
50
+ if (confirmedIds.has(systemId)) return "anomaly";
51
+ if (suspiciousIds.has(systemId)) return "suspicious";
52
+ return;
53
+ },
54
+ loading: confirmedLoading || suspiciousLoading,
55
+ };
56
+ }, [confirmed, suspicious, confirmedLoading, suspiciousLoading]);
57
+
58
+ return (
59
+ <AnomalyBadgeDataContext.Provider value={contextValue}>
60
+ {children}
61
+ </AnomalyBadgeDataContext.Provider>
62
+ );
63
+ };
64
+
65
+ export function useAnomalyBadgeDataOptional():
66
+ | AnomalyBadgeDataContextValue
67
+ | undefined {
68
+ return useContext(AnomalyBadgeDataContext);
69
+ }
@@ -28,17 +28,20 @@ type Props = SlotContext<typeof SystemSignalsSlot>;
28
28
  export const AnomalySignalsFiller: React.FC<Props> = ({
29
29
  systemIds,
30
30
  onSignals,
31
+ onLoadingChange,
31
32
  }) => {
32
33
  const anomalyClient = usePluginClient(AnomalyApi);
33
34
 
34
- const { data: confirmed = [] } = anomalyClient.getAnomalies.useQuery(
35
- { state: "anomaly", limit: 500 },
36
- { staleTime: 30_000 },
37
- );
38
- const { data: suspicious = [] } = anomalyClient.getAnomalies.useQuery(
39
- { state: "suspicious", limit: 500 },
40
- { staleTime: 30_000 },
41
- );
35
+ const { data: confirmed = [], isLoading: confirmedLoading } =
36
+ anomalyClient.getAnomalies.useQuery(
37
+ { state: "anomaly", limit: 500 },
38
+ { staleTime: 30_000 },
39
+ );
40
+ const { data: suspicious = [], isLoading: suspiciousLoading } =
41
+ anomalyClient.getAnomalies.useQuery(
42
+ { state: "suspicious", limit: 500 },
43
+ { staleTime: 30_000 },
44
+ );
42
45
 
43
46
  const signals = useMemo<SystemSignalsMap>(() => {
44
47
  const inOverview = new Set(systemIds);
@@ -71,5 +74,13 @@ export const AnomalySignalsFiller: React.FC<Props> = ({
71
74
  onSignals(ANOMALY_SIGNAL_SOURCE_ID, signals);
72
75
  }, [signals, onSignals]);
73
76
 
77
+ // Report load state so the dashboard holds its overview skeleton until this
78
+ // (and every other source) has settled, instead of flashing "all healthy".
79
+ const isLoading = confirmedLoading || suspiciousLoading;
80
+ useEffect(() => {
81
+ if (systemIds.length === 0) return;
82
+ onLoadingChange(ANOMALY_SIGNAL_SOURCE_ID, isLoading);
83
+ }, [isLoading, systemIds.length, onLoadingChange]);
84
+
74
85
  return null;
75
86
  };
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from "react";
1
+ import { useState } from "react";
2
2
  import {
3
3
  Card,
4
4
  CardHeader,
@@ -8,6 +8,7 @@ import {
8
8
  CardFooter,
9
9
  Button,
10
10
  useToast,
11
+ useInitOnceForKey,
11
12
  } from "@checkstack/ui";
12
13
  import { Activity, Save } from "lucide-react";
13
14
  import type { HealthCheckConfigIDEContext } from "@checkstack/healthcheck-frontend";
@@ -37,7 +38,7 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
37
38
 
38
39
  const { data: configRecord, isLoading } = anomalyClient.getAnomalyConfig.useQuery(
39
40
  { configurationId: context.configurationId },
40
- { enabled: !!context.configurationId },
41
+ { enabled: !!context.configurationId, gcTime: 0 },
41
42
  );
42
43
 
43
44
  const availableFields = useAnomalyFields(context.configurationId);
@@ -47,18 +48,16 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
47
48
  onError: () => toast.error("Failed to save settings"),
48
49
  });
49
50
 
50
- useEffect(() => {
51
- if (configRecord?.data) {
52
- setValues({
53
- enabled: configRecord.data.enabled ?? true,
54
- baselineWindow: configRecord.data.baselineWindow ?? "7d",
55
- notify: configRecord.data.notify ?? true,
56
- fieldOverrides:
57
- (configRecord.data.fieldOverrides as Record<string, AnomalyFieldConfig>) ??
58
- {},
59
- });
60
- }
61
- }, [configRecord]);
51
+ // Seed the form once per configuration id; ignores background refetches so
52
+ // in-progress edits aren't wiped by a realtime-driven query update.
53
+ useInitOnceForKey(configRecord?.data, context.configurationId, (data) => {
54
+ setValues({
55
+ enabled: data.enabled ?? true,
56
+ baselineWindow: data.baselineWindow ?? "7d",
57
+ notify: data.notify ?? true,
58
+ fieldOverrides: (data.fieldOverrides as Record<string, AnomalyFieldConfig>) ?? {},
59
+ });
60
+ });
62
61
 
63
62
  const handleChange = <K extends keyof AnomalySettingsFormValues>(
64
63
  key: K,
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { AnomalyBadgeDataProvider } from "./AnomalyBadgeDataProvider";
3
+
4
+ /**
5
+ * Fills catalog's `CatalogBrowseDataBoundarySlot`. Wraps the boundary's
6
+ * `children` (the catalog browse / manage tree) in `AnomalyBadgeDataProvider`,
7
+ * so the per-row `SystemAnomalyBadge`s read the anomaly lookup from context
8
+ * (`useAnomalyBadgeDataOptional`) instead of each instantiating its own live
9
+ * query observers - eliminating the per-row observer + array-scan cost.
10
+ *
11
+ * `children` is typed OPTIONAL so the component stays assignable to the slot
12
+ * context (which declares only `systemIds` / `groupIds`); catalog-frontend
13
+ * supplies the children when it folds this filler around the tree. The anomaly
14
+ * fetch is unscoped, so `systemIds` / `groupIds` are unused here.
15
+ */
16
+ export const CatalogBrowseAnomalyDataFiller = ({
17
+ children,
18
+ }: {
19
+ systemIds: string[];
20
+ groupIds: string[];
21
+ children?: React.ReactNode;
22
+ }) => <AnomalyBadgeDataProvider>{children}</AnomalyBadgeDataProvider>;
@@ -1,49 +1,53 @@
1
1
  import React, { useMemo } from "react";
2
2
  import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
3
3
  import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
4
- import { AnomalyApi, type AnomalyState } from "@checkstack/anomaly-common";
4
+ import { AnomalyApi } from "@checkstack/anomaly-common";
5
5
  import { StatusBadge } from "@checkstack/ui";
6
6
  import { ChartSpline } from "lucide-react";
7
+ import { useAnomalyBadgeDataOptional } from "./AnomalyBadgeDataProvider";
7
8
 
8
9
  type Props = SlotContext<typeof SystemStateBadgesSlot>;
9
10
 
10
11
  /**
11
12
  * Renders an anomaly status badge for a system.
12
13
  *
13
- * Fetches ONLY active anomalies (state = "anomaly") and suspicious states
14
- * in two separate queries. React Query deduplicates these globally across
15
- * all badge instances on the dashboard, so even with 50+ badges we only
16
- * issue 2 requests total. Recovered anomalies are excluded to prevent
17
- * unbounded growth in the response payload.
14
+ * On the catalog browse / manage tree an `AnomalyBadgeDataProvider` (installed
15
+ * via `CatalogBrowseDataBoundarySlot`) fetches the active + suspicious anomaly
16
+ * sets ONCE and exposes an O(1) per-system lookup; when present this badge reads
17
+ * from it and disables its own queries, so a table of rows instantiates NO
18
+ * per-row query observers. Without the provider (e.g. the system detail page)
19
+ * the fallback queries run, deduped across all badge instances by React Query,
20
+ * so even 50+ badges issue only 2 requests total. Recovered anomalies are
21
+ * excluded to keep the payload bounded.
18
22
  */
19
23
  export const SystemAnomalyBadge: React.FC<Props> = ({ system }) => {
20
24
  const anomalyClient = usePluginClient(AnomalyApi);
25
+ const badgeCtx = useAnomalyBadgeDataOptional();
21
26
 
22
- // Fetch confirmed anomalies — deduped across all badge instances via React Query
27
+ // Fallback queries only when no bulk provider is present. Disabled (no live
28
+ // observer created) on surfaces that install the provider.
23
29
  const { data: confirmedAnomalies = [] } = anomalyClient.getAnomalies.useQuery(
24
30
  { state: "anomaly", limit: 500 },
25
- { staleTime: 30_000 },
31
+ { staleTime: 30_000, enabled: !badgeCtx },
26
32
  );
27
-
28
- // Fetch suspicious states — deduped across all badge instances via React Query
29
33
  const { data: suspiciousAnomalies = [] } =
30
34
  anomalyClient.getAnomalies.useQuery(
31
35
  { state: "suspicious", limit: 500 },
32
- { staleTime: 30_000 },
36
+ { staleTime: 30_000, enabled: !badgeCtx },
33
37
  );
34
38
 
35
- // Find the worst state for this specific system
39
+ // Worst state for this system: from the provider's O(1) lookup when present,
40
+ // else scan the (deduped) fallback results.
36
41
  const systemState = useMemo(() => {
37
- // Check confirmed anomalies first (worst state)
42
+ if (badgeCtx) return badgeCtx.getSystemState(system?.id ?? "");
38
43
  if (confirmedAnomalies.some((a) => a.systemId === system?.id)) {
39
- return "anomaly" as AnomalyState;
44
+ return "anomaly" as const;
40
45
  }
41
- // Check suspicious states
42
46
  if (suspiciousAnomalies.some((a) => a.systemId === system?.id)) {
43
- return "suspicious" as AnomalyState;
47
+ return "suspicious" as const;
44
48
  }
45
49
  return;
46
- }, [confirmedAnomalies, suspiciousAnomalies, system?.id]);
50
+ }, [badgeCtx, confirmedAnomalies, suspiciousAnomalies, system?.id]);
47
51
 
48
52
  if (!systemState) return <></>;
49
53
 
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useEffect } from "react";
2
2
  import {
3
3
  usePluginClient,
4
4
  useApi,
@@ -14,7 +14,7 @@ import {
14
14
  import { healthcheckRoutes } from "@checkstack/healthcheck-common";
15
15
  import { resolveRoute } from "@checkstack/common";
16
16
  import {
17
- Card,
17
+ DetailCard,
18
18
  CardHeader,
19
19
  CardTitle,
20
20
  CardContent,
@@ -311,7 +311,10 @@ function AnomalyRow({
311
311
  // Main Widget
312
312
  // ─────────────────────────────────────────────────────────────────────────────
313
313
 
314
- export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
314
+ export const SystemAnomalyWidget: React.FC<Props> = ({
315
+ system,
316
+ onLoadingChange,
317
+ }) => {
315
318
  const anomalyClient = usePluginClient(AnomalyApi);
316
319
  const catalogClient = usePluginClient(CatalogApi);
317
320
  const toast = useToast();
@@ -404,12 +407,18 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
404
407
 
405
408
  const isLoading = loadingConfirmed || loadingSuspicious;
406
409
 
410
+ // Report load state so the detail page reveals all overview cards together
411
+ // instead of each popping in as its own fetch settles.
412
+ useEffect(() => {
413
+ onLoadingChange?.("anomaly.widget", isLoading);
414
+ }, [isLoading, onLoadingChange]);
415
+
407
416
  // Confirmed anomalies first, then suspicious
408
417
  const activeAnomalies = [...confirmedAnomalies, ...suspiciousAnomalies];
409
418
 
410
419
  if (isLoading) {
411
420
  return (
412
- <Card className="relative overflow-hidden rounded-[var(--d-card-r)] border border-border/70 bg-gradient-to-b from-surface-2 to-surface shadow-[0_1px_2px_hsl(var(--foreground)/0.04),0_10px_30px_-14px_hsl(var(--foreground)/0.12)]">
421
+ <DetailCard className="relative overflow-hidden">
413
422
  <CardHeader>
414
423
  <CardTitle className="flex items-center gap-2 text-base">
415
424
  <Activity className="h-4 w-4" />
@@ -421,7 +430,7 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
421
430
  Loading anomalies...
422
431
  </div>
423
432
  </CardContent>
424
- </Card>
433
+ </DetailCard>
425
434
  );
426
435
  }
427
436
 
@@ -440,7 +449,7 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
440
449
  const suspiciousStyles = anomalyToneStyles("unknown");
441
450
 
442
451
  return (
443
- <Card className="relative overflow-hidden rounded-[var(--d-card-r)] border border-border/70 bg-gradient-to-b from-surface-2 to-surface shadow-[0_1px_2px_hsl(var(--foreground)/0.04),0_10px_30px_-14px_hsl(var(--foreground)/0.12)] transition-all">
452
+ <DetailCard className="relative overflow-hidden transition-all">
444
453
  {/* Status accent stripe: worst active state encoded by hue + position. */}
445
454
  <span
446
455
  className={cn("absolute inset-y-0 left-0 w-1", accent.accent)}
@@ -547,6 +556,6 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
547
556
  ))}
548
557
  </div>
549
558
  </CardContent>
550
- </Card>
559
+ </DetailCard>
551
560
  );
552
561
  };
package/src/plugin.tsx CHANGED
@@ -13,10 +13,12 @@ import {
13
13
  SystemStateBadgesSlot,
14
14
  SystemDetailsSlot,
15
15
  SystemSignalsSlot,
16
+ CatalogBrowseDataBoundarySlot,
16
17
  } from "@checkstack/catalog-common";
17
18
  import { registerSubscriptionSubControls } from "@checkstack/notification-frontend";
18
19
  import { anomalySystemSubscription } from "@checkstack/anomaly-common";
19
20
  import { SystemAnomalyBadge } from "./components/SystemAnomalyBadge";
21
+ import { CatalogBrowseAnomalyDataFiller } from "./components/CatalogBrowseAnomalyDataFiller";
20
22
  import { SystemAnomalyWidget } from "./components/SystemAnomalyWidget";
21
23
  import { AnomalyFieldMuteList } from "./components/AnomalyFieldMuteList";
22
24
  import { IDETreeNode } from "@checkstack/ui";
@@ -47,6 +49,13 @@ export const plugin: FrontendPlugin = {
47
49
  id: "anomaly.system-badge",
48
50
  component: SystemAnomalyBadge,
49
51
  }),
52
+ // Bulk-fills the catalog browse/manage tree so the per-row anomaly badges
53
+ // read an O(1) lookup from context instead of each instantiating two live
54
+ // query observers + scanning the anomaly arrays (the SystemsTab mount cost).
55
+ createSlotExtension(CatalogBrowseDataBoundarySlot, {
56
+ id: "anomaly.catalog.browse-anomaly-data",
57
+ component: CatalogBrowseAnomalyDataFiller,
58
+ }),
50
59
  createSlotExtension(SystemSignalsSlot, {
51
60
  id: "anomaly.dashboard.signals",
52
61
  load: () =>