@checkstack/dashboard-frontend 0.7.8 → 0.8.0

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/src/Dashboard.tsx CHANGED
@@ -1,38 +1,23 @@
1
- import React, { useState, useMemo } from "react";
2
- import { useNavigate } from "react-router-dom";
3
- import {
4
- useApi,
5
- usePluginClient,
6
- ExtensionSlot,
7
- } from "@checkstack/frontend-api";
1
+ import React, { useCallback, useMemo, useState } from "react";
2
+ import { Link, useNavigate } from "react-router-dom";
3
+ import { usePluginClient, ExtensionSlot } from "@checkstack/frontend-api";
8
4
  import {
9
5
  CatalogApi,
10
6
  catalogRoutes,
11
- SystemStateBadgesSlot,
12
- System,
13
- Group,
14
- catalogGroupTarget,
7
+ SystemSignalsSlot,
8
+ type SystemSignalsMap,
9
+ type SystemSignalTone,
15
10
  } from "@checkstack/catalog-common";
16
11
  import { resolveRoute } from "@checkstack/common";
17
12
  import { TipBanner } from "@checkstack/tips-frontend";
18
13
  import { pluginMetadata as dashboardTipMetadata } from "./pluginMetadata";
19
- import { NotificationSubscriptionsManager } from "@checkstack/notification-frontend";
20
- import { IncidentApi } from "@checkstack/incident-common";
21
- import { MaintenanceApi } from "@checkstack/maintenance-common";
22
- import { AnomalyApi } from "@checkstack/anomaly-common";
23
14
  import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/healthcheck-common";
24
15
  import { useSignal } from "@checkstack/signal-frontend";
25
16
  import {
26
- Card,
27
- CardHeader,
28
- CardTitle,
29
- CardContent,
30
17
  SectionHeader,
31
- StatusCard,
32
18
  EmptyState,
33
19
  Button,
34
20
  LoadingSpinner,
35
- AnimatedCounter,
36
21
  TerminalFeed,
37
22
  type TerminalEntry,
38
23
  usePerformance,
@@ -42,26 +27,23 @@ import {
42
27
  LayoutGrid,
43
28
  Server,
44
29
  Activity,
45
- ChevronRight,
46
- AlertTriangle,
47
- Wrench,
48
30
  Terminal,
49
- ActivitySquare,
50
31
  Lightbulb,
32
+ ArrowRight,
51
33
  } from "lucide-react";
52
- import { authApiRef } from "@checkstack/auth-frontend/api";
53
34
  import { QueueLagAlert } from "@checkstack/queue-frontend";
54
- import { SystemBadgeDataProvider } from "./components/SystemBadgeDataProvider";
55
- import { IncidentOverviewSheet } from "./components/IncidentOverviewSheet";
56
- import { MaintenanceOverviewSheet } from "./components/MaintenanceOverviewSheet";
57
- import { AnomalyOverviewSheet } from "./components/AnomalyOverviewSheet";
35
+ import { FleetHealthHeader } from "./components/FleetHealthHeader";
36
+ import { ProblemSystemCard } from "./components/ProblemSystemCard";
37
+ import { DashboardAllClear } from "./components/DashboardAllClear";
38
+ import {
39
+ buildProblemSystems,
40
+ countByTone,
41
+ mergeSignalsBySystem,
42
+ type SignalsBySource,
43
+ } from "./logic/systemSignals";
58
44
 
59
45
  const MAX_TERMINAL_ENTRIES = 8;
60
46
 
61
- interface GroupWithSystems extends Group {
62
- systems: System[];
63
- }
64
-
65
47
  const statusToVariant = (
66
48
  status: string,
67
49
  ): "default" | "success" | "warning" | "error" => {
@@ -84,103 +66,59 @@ const statusToVariant = (
84
66
  export const Dashboard: React.FC = () => {
85
67
  const { isLowPower } = usePerformance();
86
68
  const catalogClient = usePluginClient(CatalogApi);
87
- const incidentClient = usePluginClient(IncidentApi);
88
- const maintenanceClient = usePluginClient(MaintenanceApi);
89
- const anomalyClient = usePluginClient(AnomalyApi);
90
-
91
69
  const navigate = useNavigate();
92
- const authApi = useApi(authApiRef);
93
- const { data: session } = authApi.useSession();
94
70
 
95
- // Terminal feed entries from real healthcheck signals
96
71
  const [terminalEntries, setTerminalEntries] = useState<TerminalEntry[]>([]);
72
+ const [activeTone, setActiveTone] = useState<SystemSignalTone | null>(null);
97
73
 
98
- const [isIncidentSheetOpen, setIncidentSheetOpen] = useState(false);
99
- const [isMaintenanceSheetOpen, setMaintenanceSheetOpen] = useState(false);
100
- const [isAnomalySheetOpen, setAnomalySheetOpen] = useState(false);
101
-
102
- // -------------------------------------------------------------------------
103
- // DATA QUERIES
104
- // -------------------------------------------------------------------------
74
+ // ----------------------------------------------------------------------- //
75
+ // DATA
76
+ // ----------------------------------------------------------------------- //
105
77
 
106
- // Fetch entities from catalog (groups and systems in one call)
107
78
  const { data: entitiesData, isLoading: entitiesLoading } =
108
79
  catalogClient.getEntities.useQuery({}, { staleTime: 30_000 });
109
- const systems = entitiesData?.systems ?? [];
110
-
111
- // Fetch active incidents
112
- const { data: incidentsData, isLoading: incidentsLoading } =
113
- incidentClient.listIncidents.useQuery(
114
- { includeResolved: false },
115
- { staleTime: 30_000 },
116
- );
117
- const incidents = incidentsData?.incidents ?? [];
118
-
119
- // Fetch active maintenances
120
- const { data: inProgressMaintenancesData, isLoading: inProgressLoading } =
121
- maintenanceClient.listMaintenances.useQuery(
122
- { status: "in_progress" },
123
- { staleTime: 30_000 },
124
- );
125
-
126
- // Fetch scheduled maintenances
127
- const { data: scheduledMaintenancesData, isLoading: scheduledLoading } =
128
- maintenanceClient.listMaintenances.useQuery(
129
- { status: "scheduled" },
130
- { staleTime: 30_000 },
131
- );
132
-
133
- const maintenances = useMemo(() => {
134
- return [
135
- ...(inProgressMaintenancesData?.maintenances ?? []),
136
- ...(scheduledMaintenancesData?.maintenances ?? []),
137
- ].toSorted(
138
- (a, b) => new Date(a.startAt).getTime() - new Date(b.startAt).getTime(),
139
- );
140
- }, [inProgressMaintenancesData, scheduledMaintenancesData]);
141
80
 
142
- const maintenancesLoading = inProgressLoading || scheduledLoading;
143
-
144
- // Fetch active anomalies
145
- const { data: anomalies = [], isLoading: anomaliesLoading } =
146
- anomalyClient.getAnomalies.useQuery(
147
- { limit: 100, state: "anomaly" },
148
- { staleTime: 30_000 },
149
- );
81
+ const systems = useMemo(() => entitiesData?.systems ?? [], [entitiesData]);
82
+ // Stable across renders (derives from the query's stable data ref) so the
83
+ // signal fillers don't re-report in a loop.
84
+ const systemIds = useMemo(() => systems.map((s) => s.id), [systems]);
85
+ const systemMap = useMemo(
86
+ () => new Map(systems.map((s) => [s.id, s])),
87
+ [systems],
88
+ );
150
89
 
151
- // Combined loading state
152
- const loading =
153
- entitiesLoading ||
154
- incidentsLoading ||
155
- maintenancesLoading ||
156
- anomaliesLoading;
90
+ // ----------------------------------------------------------------------- //
91
+ // SIGNAL AGGREGATION (extensible: any plugin fills SystemSignalsSlot)
92
+ // ----------------------------------------------------------------------- //
157
93
 
158
- // -------------------------------------------------------------------------
159
- // COMPUTED DATA
160
- // -------------------------------------------------------------------------
94
+ const [signalsBySource, setSignalsBySource] = useState<SignalsBySource>({});
161
95
 
162
- // Derived statistics
163
- const systemsCount = systems.length;
164
- const activeIncidentsCount = incidents.length;
165
- const activeMaintenancesCount = maintenances.length;
166
- const activeAnomaliesCount = anomalies.length;
96
+ const handleSignals = useCallback(
97
+ (sourceId: string, signals: SystemSignalsMap) => {
98
+ setSignalsBySource((prev) =>
99
+ prev[sourceId] === signals ? prev : { ...prev, [sourceId]: signals },
100
+ );
101
+ },
102
+ [],
103
+ );
167
104
 
168
- // Map groups to include their systems
169
- const groupsWithSystems = useMemo<GroupWithSystems[]>(() => {
170
- const groups = entitiesData?.groups ?? [];
171
- const systems = entitiesData?.systems ?? [];
172
- const systemMap = new Map(systems.map((s) => [s.id, s]));
173
- return groups.map((group) => {
174
- const groupSystems = (group.systemIds || [])
175
- .map((id) => systemMap.get(id))
176
- .filter((s): s is System => s !== undefined);
177
- return { ...group, systems: groupSystems };
178
- });
179
- }, [entitiesData]);
105
+ const problems = useMemo(
106
+ () => buildProblemSystems(mergeSignalsBySystem(signalsBySource)),
107
+ [signalsBySource],
108
+ );
109
+ const counts = useMemo(() => countByTone(problems), [problems]);
110
+
111
+ const visibleProblems = useMemo(
112
+ () =>
113
+ activeTone
114
+ ? problems.filter((p) => p.worstTone === activeTone)
115
+ : problems,
116
+ [problems, activeTone],
117
+ );
180
118
 
181
- // -------------------------------------------------------------------------
182
- // SIGNAL HANDLERS
183
- // -------------------------------------------------------------------------
119
+ // ----------------------------------------------------------------------- //
120
+ // SIGNALS
121
+ // ----------------------------------------------------------------------- //
184
122
 
185
123
  useSignal(
186
124
  HEALTH_CHECK_RUN_COMPLETED,
@@ -192,43 +130,40 @@ export const Dashboard: React.FC = () => {
192
130
  variant: statusToVariant(status),
193
131
  suffix: latencyMs === undefined ? undefined : `${latencyMs}ms`,
194
132
  };
195
-
196
133
  setTerminalEntries((prev) =>
197
134
  [newEntry, ...prev].slice(0, MAX_TERMINAL_ENTRIES),
198
135
  );
199
136
  },
200
137
  );
201
138
 
202
- // -------------------------------------------------------------------------
203
- // HANDLERS
204
- // -------------------------------------------------------------------------
205
-
206
- const handleSystemClick = (systemId: string) => {
207
- navigate(resolveRoute(catalogRoutes.routes.systemDetail, { systemId }));
208
- };
139
+ const systemsCount = systems.length;
140
+ const healthyCount = Math.max(systemsCount - problems.length, 0);
141
+ const catalogHref = resolveRoute(catalogRoutes.routes.home);
209
142
 
210
- // -------------------------------------------------------------------------
143
+ // ----------------------------------------------------------------------- //
211
144
  // RENDER
212
- // -------------------------------------------------------------------------
145
+ // ----------------------------------------------------------------------- //
213
146
 
214
- const renderGroupsContent = () => {
215
- if (loading) {
147
+ const renderOverview = () => {
148
+ if (entitiesLoading) {
216
149
  return <LoadingSpinner />;
217
150
  }
218
151
 
219
- if (groupsWithSystems.length === 0) {
152
+ if (systemsCount === 0) {
220
153
  return (
221
154
  <EmptyState
222
155
  icon={<Server className="w-12 h-12" />}
223
156
  title="Nothing to show on the dashboard yet"
224
- description="Once you have systems organised into groups, this is where you'll see their rolled-up health, on a per-team or per-product basis."
157
+ description="Add the systems you want to monitor and attach health checks. Anything that needs attention will surface here automatically."
225
158
  steps={[
226
159
  "Open the Catalog and add the systems you want to monitor.",
227
160
  "Group related systems together (e.g. one group per team).",
228
- "Attach health checks to each system so the dashboard turns green when things are working — and red when they aren't.",
161
+ "Attach health checks so the dashboard surfaces issues the moment they appear.",
229
162
  ]}
230
163
  actions={
231
- <Button onClick={() => navigate(resolveRoute(catalogRoutes.routes.config))}>
164
+ <Button
165
+ onClick={() => navigate(resolveRoute(catalogRoutes.routes.config))}
166
+ >
232
167
  <LayoutGrid className="w-4 h-4 mr-2" />
233
168
  Open Catalog
234
169
  </Button>
@@ -237,271 +172,130 @@ export const Dashboard: React.FC = () => {
237
172
  );
238
173
  }
239
174
 
240
- // Collect all system IDs for bulk data fetching
241
- const allSystemIds = groupsWithSystems.flatMap((g) =>
242
- g.systems.map((s) => s.id),
243
- );
244
-
245
175
  return (
246
- <SystemBadgeDataProvider systemIds={allSystemIds}>
247
- <div className="space-y-4">
248
- {groupsWithSystems.map((group) => (
249
- <Card
250
- key={group.id}
251
- className={cn(
252
- "border-border shadow-sm",
253
- !isLowPower && "hover:shadow-md transition-shadow",
254
- )}
255
- >
256
- <CardHeader className="border-b border-border bg-muted/30">
257
- <div className="flex items-center gap-2">
258
- <LayoutGrid className="h-5 w-5 text-muted-foreground" />
259
- <CardTitle className="text-lg font-semibold text-foreground">
260
- {group.name}
261
- </CardTitle>
262
- <span className="ml-auto text-sm text-muted-foreground mr-2">
263
- {group.systems.length}{" "}
264
- {group.systems.length === 1 ? "system" : "systems"}
265
- </span>
266
- {session && (
267
- <NotificationSubscriptionsManager
268
- target={catalogGroupTarget}
269
- resource={{
270
- groupId: group.id,
271
- groupName: group.name,
272
- }}
273
- />
274
- )}
275
- </div>
276
- </CardHeader>
277
- <CardContent className="p-0">
278
- {group.systems.length === 0 ? (
279
- <div className="py-8 text-center">
280
- <p className="text-sm text-muted-foreground">
281
- No systems in this group yet
282
- </p>
283
- </div>
284
- ) : (
285
- <div className="flex flex-col divide-y divide-border">
286
- {group.systems.map((system) => (
287
- <button
288
- key={system.id}
289
- onClick={() => handleSystemClick(system.id)}
290
- className="flex items-center gap-4 px-4 py-3 bg-card hover:bg-muted/50 transition-colors text-left w-full group"
291
- >
292
- <div className="flex items-center gap-3 min-w-32 flex-shrink-0">
293
- <Activity className="h-4 w-4 text-muted-foreground flex-shrink-0" />
294
- <p className="text-sm font-medium text-foreground truncate">
295
- {system.name}
296
- </p>
297
- </div>
298
- <div className="flex items-center gap-2 flex-wrap flex-1 justify-end min-w-0">
299
- <ExtensionSlot
300
- slot={SystemStateBadgesSlot}
301
- context={{ system }}
302
- />
303
- </div>
304
- <ChevronRight
305
- className={cn(
306
- "h-4 w-4 text-muted-foreground opacity-50 group-hover:opacity-100 flex-shrink-0 ml-2",
307
- !isLowPower && "transition-opacity",
308
- )}
309
- />
310
- </button>
311
- ))}
312
- </div>
313
- )}
314
- </CardContent>
315
- </Card>
316
- ))}
317
- </div>
318
- </SystemBadgeDataProvider>
176
+ <div className="space-y-4">
177
+ <FleetHealthHeader
178
+ systemsCount={systemsCount}
179
+ problemsCount={problems.length}
180
+ counts={counts}
181
+ healthyCount={healthyCount}
182
+ activeTone={activeTone}
183
+ onToneSelect={setActiveTone}
184
+ isLowPower={isLowPower}
185
+ />
186
+ {problems.length === 0 ? (
187
+ <DashboardAllClear
188
+ systemsCount={systemsCount}
189
+ isLowPower={isLowPower}
190
+ />
191
+ ) : (
192
+ <div className="grid gap-4 md:grid-cols-2">
193
+ {visibleProblems.map((problem) => {
194
+ const system = systemMap.get(problem.systemId);
195
+ if (!system) return null;
196
+ return (
197
+ <ProblemSystemCard
198
+ key={problem.systemId}
199
+ system={system}
200
+ problem={problem}
201
+ isLowPower={isLowPower}
202
+ />
203
+ );
204
+ })}
205
+ </div>
206
+ )}
207
+ </div>
319
208
  );
320
209
  };
321
210
 
322
211
  return (
323
- <>
324
- <div
325
- className={cn(
326
- "space-y-8",
327
- !isLowPower && "animate-in fade-in duration-500",
328
- )}
329
- >
330
- {/* Queue Lag Warning */}
331
- <QueueLagAlert />
332
-
333
- {/* First-run welcome */}
334
- <TipBanner
335
- plugin={dashboardTipMetadata}
336
- id="welcome"
337
- title="Welcome to Checkstack"
338
- description={
339
- <>
340
- This is your dashboard — overall health, recent activity, and
341
- ongoing maintenances at a glance. To bring it to life, start
342
- by adding a system in the <strong>Catalog</strong>, then
343
- attach a health check to it.
344
- </>
345
- }
346
- action={{
347
- label: "Open Catalog",
348
- onClick: () => navigate(resolveRoute(catalogRoutes.routes.config)),
349
- }}
350
- actionHint={
351
- <span className="inline-flex items-center gap-1.5">
352
- <Lightbulb
353
- className="size-3.5 shrink-0 text-amber-500"
354
- aria-hidden="true"
355
- />
356
- See a small lightbulb next to a button or control? Click it
357
- for a short explanation of what that feature does.
358
- </span>
359
- }
360
- />
361
-
362
- {/* Overview Section */}
363
- <section>
364
- <SectionHeader
365
- title="Overview"
366
- icon={<Activity className="w-5 h-5" />}
367
- />
368
- <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
369
- <StatusCard
370
- title="Total Systems"
371
- value={loading ? "..." : <AnimatedCounter value={systemsCount} />}
372
- description="Monitored systems in your catalog"
373
- icon={<Server className="w-4 h-4" />}
374
- />
375
-
376
- <StatusCard
377
- variant={activeIncidentsCount > 0 ? "gradient" : "default"}
378
- title="Active Incidents"
379
- value={
380
- loading ? (
381
- "..."
382
- ) : (
383
- <AnimatedCounter value={activeIncidentsCount} />
384
- )
385
- }
386
- description={
387
- activeIncidentsCount === 0
388
- ? "All systems operating normally"
389
- : "Unresolved issues requiring attention"
390
- }
391
- icon={<AlertTriangle className="w-4 h-4" />}
392
- onClick={
393
- activeIncidentsCount > 0
394
- ? () => setIncidentSheetOpen(true)
395
- : undefined
396
- }
397
- className={cn(
398
- activeIncidentsCount > 0 && "cursor-pointer hover:opacity-90",
399
- activeIncidentsCount > 0 && !isLowPower && "hover:scale-[1.02]",
400
- )}
212
+ <div
213
+ className={cn(
214
+ "space-y-8",
215
+ !isLowPower && "animate-in fade-in duration-500",
216
+ )}
217
+ >
218
+ <QueueLagAlert />
219
+
220
+ <TipBanner
221
+ plugin={dashboardTipMetadata}
222
+ id="welcome"
223
+ title="Welcome to Checkstack"
224
+ description={
225
+ <>
226
+ This is your dashboard. It surfaces only the systems that need
227
+ attention right now - everything healthy stays out of your way. To
228
+ bring it to life, add a system in the <strong>Catalog</strong>, then
229
+ attach a health check to it.
230
+ </>
231
+ }
232
+ action={{
233
+ label: "Open Catalog",
234
+ onClick: () => navigate(resolveRoute(catalogRoutes.routes.config)),
235
+ }}
236
+ actionHint={
237
+ <span className="inline-flex items-center gap-1.5">
238
+ <Lightbulb
239
+ className="size-3.5 shrink-0 text-amber-500"
240
+ aria-hidden="true"
401
241
  />
242
+ See a small lightbulb next to a button or control? Click it for a
243
+ short explanation of what that feature does.
244
+ </span>
245
+ }
246
+ />
402
247
 
403
- <StatusCard
404
- variant={activeMaintenancesCount > 0 ? "gradient" : "default"}
405
- title="Active & Scheduled Maintenances"
406
- value={
407
- loading ? (
408
- "..."
409
- ) : (
410
- <AnimatedCounter value={activeMaintenancesCount} />
411
- )
412
- }
413
- description={
414
- activeMaintenancesCount === 0
415
- ? "No scheduled maintenance"
416
- : "Ongoing or upcoming maintenance windows"
417
- }
418
- icon={<Wrench className="w-4 h-4" />}
419
- onClick={
420
- activeMaintenancesCount > 0
421
- ? () => setMaintenanceSheetOpen(true)
422
- : undefined
423
- }
424
- className={cn(
425
- activeMaintenancesCount > 0 && "cursor-pointer hover:opacity-90",
426
- activeMaintenancesCount > 0 &&
427
- !isLowPower &&
428
- "hover:scale-[1.02]",
429
- )}
430
- />
248
+ {/*
249
+ Headless, render-once aggregation boundary. Every plugin fills
250
+ SystemSignalsSlot with its bulk per-system state; the dashboard merges
251
+ the reports to drive the overview. The dashboard is agnostic to which
252
+ plugins contribute - third-party plugins participate the same way.
253
+ */}
254
+ <ExtensionSlot
255
+ slot={SystemSignalsSlot}
256
+ context={{ systemIds, onSignals: handleSignals }}
257
+ />
431
258
 
432
- <StatusCard
433
- variant={activeAnomaliesCount > 0 ? "gradient" : "default"}
434
- title="Active Anomalies"
435
- value={
436
- loading ? (
437
- "..."
438
- ) : (
439
- <AnimatedCounter value={activeAnomaliesCount} />
440
- )
441
- }
442
- description="Unusual behavior detected"
443
- icon={<ActivitySquare className="w-4 h-4" />}
444
- onClick={
445
- activeAnomaliesCount > 0
446
- ? () => setAnomalySheetOpen(true)
447
- : undefined
448
- }
259
+ <section>
260
+ <div className="mb-6 flex items-center justify-between gap-4">
261
+ <div className="flex items-center gap-2">
262
+ <span className="text-primary">
263
+ <Activity className="h-5 w-5" />
264
+ </span>
265
+ <h2 className="text-xl font-semibold tracking-tight text-foreground">
266
+ System health
267
+ </h2>
268
+ </div>
269
+ {systemsCount > 0 && (
270
+ <Link
271
+ to={catalogHref}
449
272
  className={cn(
450
- activeAnomaliesCount > 0 &&
451
- "cursor-pointer hover:opacity-90 bg-gradient-to-br from-warning/20 to-warning/5 border-warning/30",
452
- activeAnomaliesCount > 0 &&
453
- !isLowPower &&
454
- "hover:scale-[1.02]",
273
+ "inline-flex shrink-0 items-center gap-1 text-sm font-medium text-primary hover:underline",
274
+ !isLowPower && "transition-colors",
455
275
  )}
456
- />
457
- </div>
458
- </section>
459
-
460
- {/* Terminal Feed and System Groups - Two Column Layout */}
461
- <div className="grid gap-8 lg:grid-cols-3">
462
- {/* Terminal Feed */}
463
- <section className="lg:col-span-1">
464
- <SectionHeader
465
- title="Recent Activity"
466
- icon={<Terminal className="w-5 h-5" />}
467
- />
468
- <TerminalFeed
469
- entries={terminalEntries}
470
- maxEntries={MAX_TERMINAL_ENTRIES}
471
- maxHeight="350px"
472
- title="checkstack status --watch"
473
- />
474
- </section>
475
-
476
- {/* System Groups */}
477
- <section className="lg:col-span-2">
478
- <SectionHeader
479
- title="System Groups"
480
- icon={<LayoutGrid className="w-5 h-5" />}
481
- />
482
- {renderGroupsContent()}
483
- </section>
276
+ >
277
+ View catalog
278
+ <ArrowRight className="h-4 w-4" />
279
+ </Link>
280
+ )}
484
281
  </div>
485
- </div>
282
+ {renderOverview()}
283
+ </section>
486
284
 
487
- <IncidentOverviewSheet
488
- open={isIncidentSheetOpen}
489
- onOpenChange={setIncidentSheetOpen}
490
- incidents={incidents}
491
- systems={systems}
492
- />
493
- <MaintenanceOverviewSheet
494
- open={isMaintenanceSheetOpen}
495
- onOpenChange={setMaintenanceSheetOpen}
496
- maintenances={maintenances}
497
- systems={systems}
498
- />
499
- <AnomalyOverviewSheet
500
- open={isAnomalySheetOpen}
501
- onOpenChange={setAnomalySheetOpen}
502
- anomalies={anomalies}
503
- systems={systems}
504
- />
505
- </>
285
+ {systemsCount > 0 && (
286
+ <section>
287
+ <SectionHeader
288
+ title="Recent activity"
289
+ icon={<Terminal className="w-5 h-5" />}
290
+ />
291
+ <TerminalFeed
292
+ entries={terminalEntries}
293
+ maxEntries={MAX_TERMINAL_ENTRIES}
294
+ maxHeight="320px"
295
+ title="checkstack status --watch"
296
+ />
297
+ </section>
298
+ )}
299
+ </div>
506
300
  );
507
301
  };