@cat-factory/app 0.122.2 → 0.123.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.
@@ -24,6 +24,10 @@ const auth = useAuthStore()
24
24
  const providerConnections = useProviderConnectionsStore()
25
25
  const ui = useUiStore()
26
26
 
27
+ // The operator dashboard (deployment-level run health) is sensitive cross-workspace data,
28
+ // so it's shown only to an admin of the active account (matching the backend admin gate).
29
+ const isAccountAdmin = computed(() => accounts.activeAccount?.roles?.includes('admin') ?? false)
30
+
27
31
  // The Infrastructure menu (agent-container execution + test environments) shows whenever the
28
32
  // deployment reports its infrastructure capability — every facade populates `auth.infrastructure`
29
33
  // (it drives the execution-backend selector), so there is always an execution + test-env backend
@@ -367,6 +371,20 @@ watch(
367
371
  >
368
372
  {{ t('nav.accountSettings') }}
369
373
  </UButton>
374
+ <!-- Platform observability: deployment-level run health. Admin-only, like the backend gate. -->
375
+ <UButton
376
+ v-if="accounts.enabled && isAccountAdmin"
377
+ block
378
+ color="primary"
379
+ variant="soft"
380
+ size="sm"
381
+ icon="i-lucide-gauge"
382
+ class="justify-start"
383
+ data-testid="nav-operator-dashboard"
384
+ @click="ui.openOperatorDashboard()"
385
+ >
386
+ {{ t('nav.operatorDashboard') }}
387
+ </UButton>
370
388
  </div>
371
389
  </section>
372
390
  </div>
@@ -0,0 +1,394 @@
1
+ <script setup lang="ts">
2
+ import { computed, watch } from 'vue'
3
+ import { onKeyStroke } from '@vueuse/core'
4
+ import type { AgentFailureKind, PlatformObservabilityWindow } from '~/types/execution'
5
+ import { formatMs } from '~/utils/observability'
6
+
7
+ // Deployment-level (platform-operator) observability dashboard: the aggregate health of the
8
+ // active account's runs — outcome totals + success rate, a time-bucketed outcome trend, the
9
+ // failure-kind taxonomy, live/parked depth, and duration stats — over a selectable window.
10
+ // Admin-gated; opened via `ui.openOperatorDashboard()` from the sidebar. The account-scoped
11
+ // counterpart of the per-run `ObservabilityPanel`.
12
+ const ui = useUiStore()
13
+ const accounts = useAccountsStore()
14
+ const platform = usePlatformObservabilityStore()
15
+ const { t, d, n } = useI18n()
16
+
17
+ const open = computed(() => ui.operatorDashboardOpen)
18
+ const view = computed(() => platform.view)
19
+ const loading = computed(() => platform.loading)
20
+ const error = computed(() => platform.error)
21
+ const accountName = computed(() => accounts.activeAccount?.name ?? '')
22
+
23
+ // Window options as static literal keys (keeps the typed-message-key check live).
24
+ const WINDOWS: { value: PlatformObservabilityWindow; label: string }[] = [
25
+ { value: '1h', label: t('platformObservability.window.oneHour') },
26
+ { value: '24h', label: t('platformObservability.window.oneDay') },
27
+ { value: '7d', label: t('platformObservability.window.sevenDays') },
28
+ ]
29
+
30
+ // Exhaustive enum→label map (tier-2 dynamic-key guard): a new AgentFailureKind fails the
31
+ // typecheck here, and an out-of-enum kind falls back to its raw code below.
32
+ const FAILURE_KIND_KEYS: Record<AgentFailureKind, string> = {
33
+ preflight: 'platformObservability.failureKind.preflight',
34
+ dispatch: 'platformObservability.failureKind.dispatch',
35
+ environment: 'platformObservability.failureKind.environment',
36
+ evicted: 'platformObservability.failureKind.evicted',
37
+ timeout: 'platformObservability.failureKind.timeout',
38
+ agent: 'platformObservability.failureKind.agent',
39
+ job_failed: 'platformObservability.failureKind.job_failed',
40
+ rejected: 'platformObservability.failureKind.rejected',
41
+ companion_rejected: 'platformObservability.failureKind.companion_rejected',
42
+ stalled: 'platformObservability.failureKind.stalled',
43
+ cancelled: 'platformObservability.failureKind.cancelled',
44
+ unknown: 'platformObservability.failureKind.unknown',
45
+ }
46
+ function failureLabel(kind: string): string {
47
+ const key = FAILURE_KIND_KEYS[kind as AgentFailureKind]
48
+ return key ? t(key) : kind
49
+ }
50
+
51
+ // The largest failure count, so each taxonomy bar is drawn relative to the leader.
52
+ const maxFailure = computed(() => Math.max(1, ...(view.value?.failures ?? []).map((f) => f.count)))
53
+ // The largest total in any trend bucket, so each stacked column scales to the tallest.
54
+ const maxTrend = computed(() =>
55
+ Math.max(1, ...(view.value?.trend.points ?? []).map((p) => p.done + p.failed + p.other)),
56
+ )
57
+
58
+ function barPct(count: number, max: number): number {
59
+ return Math.round((count / max) * 100)
60
+ }
61
+ function heightPct(count: number, max: number): number {
62
+ // Floor a non-zero column to 4% so a single run is still visible in the sparkline.
63
+ return count === 0 ? 0 : Math.max(4, Math.round((count / max) * 100))
64
+ }
65
+ function trendTooltip(p: { start: number; done: number; failed: number; other: number }): string {
66
+ return `${d(new Date(p.start), 'short')} · ${t('platformObservability.trend.done')} ${p.done} · ${t('platformObservability.trend.failed')} ${p.failed} · ${t('platformObservability.trend.other')} ${p.other}`
67
+ }
68
+
69
+ function setWindow(w: PlatformObservabilityWindow) {
70
+ void platform.setWindow(w)
71
+ }
72
+ function refresh() {
73
+ void platform.load()
74
+ }
75
+ function close() {
76
+ ui.closeOperatorDashboard()
77
+ }
78
+ onKeyStroke('Escape', () => {
79
+ if (open.value) close()
80
+ })
81
+
82
+ // Load (and refresh) whenever the dashboard opens.
83
+ watch(
84
+ open,
85
+ (isOpen) => {
86
+ if (isOpen) void platform.load()
87
+ },
88
+ { immediate: true },
89
+ )
90
+ </script>
91
+
92
+ <template>
93
+ <Teleport to="body">
94
+ <Transition name="obs-fade">
95
+ <div
96
+ v-if="open"
97
+ class="fixed inset-0 z-[60] flex flex-col bg-slate-950/96 backdrop-blur-sm"
98
+ role="dialog"
99
+ aria-modal="true"
100
+ data-testid="operator-dashboard"
101
+ >
102
+ <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
103
+ <div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-sky-500/15">
104
+ <UIcon name="i-lucide-gauge" class="h-5 w-5 text-sky-400" />
105
+ </div>
106
+ <div class="min-w-0">
107
+ <h1 class="truncate text-base font-semibold text-white">
108
+ {{ t('platformObservability.title') }}
109
+ </h1>
110
+ <p v-if="accountName" class="truncate text-xs text-slate-500">{{ accountName }}</p>
111
+ </div>
112
+ <div class="ms-auto flex items-center gap-1.5">
113
+ <div class="me-1 flex rounded-lg border border-slate-800 p-0.5 text-[12px]">
114
+ <button
115
+ v-for="opt in WINDOWS"
116
+ :key="opt.value"
117
+ class="rounded-md px-2.5 py-1 transition"
118
+ :class="
119
+ platform.window === opt.value
120
+ ? 'bg-slate-800 text-slate-100'
121
+ : 'text-slate-400 hover:text-slate-200'
122
+ "
123
+ :data-testid="`operator-window-${opt.value}`"
124
+ @click="setWindow(opt.value)"
125
+ >
126
+ {{ opt.label }}
127
+ </button>
128
+ </div>
129
+ <button
130
+ class="rounded-lg border border-slate-800 p-1.5 text-slate-400 transition hover:text-slate-200"
131
+ :title="t('platformObservability.refresh')"
132
+ :aria-label="t('platformObservability.refresh')"
133
+ data-testid="operator-refresh"
134
+ @click="refresh"
135
+ >
136
+ <UIcon
137
+ name="i-lucide-refresh-cw"
138
+ class="h-4 w-4"
139
+ :class="{ 'animate-spin': loading }"
140
+ />
141
+ </button>
142
+ <button
143
+ class="rounded-lg border border-slate-800 p-1.5 text-slate-400 transition hover:text-slate-200"
144
+ :title="t('platformObservability.close')"
145
+ :aria-label="t('platformObservability.close')"
146
+ data-testid="operator-close"
147
+ @click="close"
148
+ >
149
+ <UIcon name="i-lucide-x" class="h-4 w-4" />
150
+ </button>
151
+ </div>
152
+ </header>
153
+
154
+ <div class="flex-1 overflow-y-auto px-6 py-5">
155
+ <div
156
+ v-if="error"
157
+ class="mx-auto max-w-2xl rounded-lg border border-rose-800/60 bg-rose-950/40 p-4 text-sm text-rose-200"
158
+ >
159
+ <p>{{ error }}</p>
160
+ <button
161
+ class="mt-2 rounded-md border border-rose-700 px-3 py-1 text-xs hover:bg-rose-900/40"
162
+ @click="refresh"
163
+ >
164
+ {{ t('platformObservability.retry') }}
165
+ </button>
166
+ </div>
167
+
168
+ <div v-else-if="loading && !view" class="py-16 text-center text-sm text-slate-400">
169
+ {{ t('platformObservability.loading') }}
170
+ </div>
171
+
172
+ <div v-else-if="view" class="mx-auto flex max-w-5xl flex-col gap-6">
173
+ <!-- Outcome summary tiles -->
174
+ <section>
175
+ <h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
176
+ {{ t('platformObservability.outcomes.title') }}
177
+ </h2>
178
+ <div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
179
+ <div class="rounded-lg border border-slate-800 bg-slate-900/40 p-3">
180
+ <p class="text-2xl font-semibold text-white">
181
+ {{ n(view.outcomes.total, 'decimal') }}
182
+ </p>
183
+ <p class="text-xs text-slate-500">
184
+ {{ t('platformObservability.outcomes.total') }}
185
+ </p>
186
+ </div>
187
+ <div class="rounded-lg border border-slate-800 bg-slate-900/40 p-3">
188
+ <p class="text-2xl font-semibold text-emerald-400">
189
+ {{ n(view.outcomes.done, 'decimal') }}
190
+ </p>
191
+ <p class="text-xs text-slate-500">
192
+ {{ t('platformObservability.outcomes.done') }}
193
+ </p>
194
+ </div>
195
+ <div class="rounded-lg border border-slate-800 bg-slate-900/40 p-3">
196
+ <p class="text-2xl font-semibold text-rose-400">
197
+ {{ n(view.outcomes.failed, 'decimal') }}
198
+ </p>
199
+ <p class="text-xs text-slate-500">
200
+ {{ t('platformObservability.outcomes.failed') }}
201
+ </p>
202
+ </div>
203
+ <div class="rounded-lg border border-slate-800 bg-slate-900/40 p-3">
204
+ <p
205
+ class="text-2xl font-semibold text-sky-400"
206
+ data-testid="operator-success-rate"
207
+ >
208
+ {{
209
+ view.outcomes.successRate == null
210
+ ? '—'
211
+ : n(view.outcomes.successRate, 'percent')
212
+ }}
213
+ </p>
214
+ <p class="text-xs text-slate-500">
215
+ {{ t('platformObservability.outcomes.successRate') }}
216
+ </p>
217
+ </div>
218
+ </div>
219
+ </section>
220
+
221
+ <!-- Outcome trend sparkline -->
222
+ <section>
223
+ <h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
224
+ {{ t('platformObservability.trend.title') }}
225
+ </h2>
226
+ <div class="rounded-lg border border-slate-800 bg-slate-900/40 p-4">
227
+ <div
228
+ v-if="view.outcomes.total === 0"
229
+ class="py-6 text-center text-xs text-slate-500"
230
+ >
231
+ {{ t('platformObservability.trend.empty') }}
232
+ </div>
233
+ <div v-else class="flex h-28 items-end gap-0.5" data-testid="operator-trend">
234
+ <div
235
+ v-for="p in view.trend.points"
236
+ :key="p.start"
237
+ class="flex flex-1 flex-col justify-end"
238
+ :title="trendTooltip(p)"
239
+ >
240
+ <div
241
+ class="w-full rounded-t-sm bg-rose-500/80"
242
+ :style="{ height: `${heightPct(p.failed, maxTrend)}%` }"
243
+ />
244
+ <div
245
+ class="w-full bg-slate-500/60"
246
+ :style="{ height: `${heightPct(p.other, maxTrend)}%` }"
247
+ />
248
+ <div
249
+ class="w-full rounded-b-sm bg-emerald-500/80"
250
+ :style="{ height: `${heightPct(p.done, maxTrend)}%` }"
251
+ />
252
+ </div>
253
+ </div>
254
+ <div class="mt-2 flex items-center gap-4 text-[11px] text-slate-500">
255
+ <span class="flex items-center gap-1"
256
+ ><span class="h-2 w-2 rounded-sm bg-emerald-500/80" />{{
257
+ t('platformObservability.trend.done')
258
+ }}</span
259
+ >
260
+ <span class="flex items-center gap-1"
261
+ ><span class="h-2 w-2 rounded-sm bg-rose-500/80" />{{
262
+ t('platformObservability.trend.failed')
263
+ }}</span
264
+ >
265
+ <span class="flex items-center gap-1"
266
+ ><span class="h-2 w-2 rounded-sm bg-slate-500/60" />{{
267
+ t('platformObservability.trend.other')
268
+ }}</span
269
+ >
270
+ </div>
271
+ </div>
272
+ </section>
273
+
274
+ <div class="grid gap-6 md:grid-cols-2">
275
+ <!-- Failure taxonomy -->
276
+ <section>
277
+ <h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
278
+ {{ t('platformObservability.failures.title') }}
279
+ </h2>
280
+ <div class="rounded-lg border border-slate-800 bg-slate-900/40 p-4">
281
+ <div v-if="!view.failures.length" class="py-4 text-center text-xs text-slate-500">
282
+ {{ t('platformObservability.failures.empty') }}
283
+ </div>
284
+ <ul v-else class="flex flex-col gap-2" data-testid="operator-failures">
285
+ <li v-for="f in view.failures" :key="f.kind" class="text-xs">
286
+ <div class="mb-0.5 flex items-center justify-between">
287
+ <span class="text-slate-300">{{ failureLabel(f.kind) }}</span>
288
+ <span class="tabular-nums text-slate-400">{{ f.count }}</span>
289
+ </div>
290
+ <div class="h-1.5 rounded-full bg-slate-800">
291
+ <div
292
+ class="h-1.5 rounded-full bg-rose-500/70"
293
+ :style="{ width: `${barPct(f.count, maxFailure)}%` }"
294
+ />
295
+ </div>
296
+ </li>
297
+ </ul>
298
+ </div>
299
+ </section>
300
+
301
+ <!-- Live depth + durations -->
302
+ <section class="flex flex-col gap-4">
303
+ <div>
304
+ <h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
305
+ {{ t('platformObservability.live.title') }}
306
+ </h2>
307
+ <div
308
+ class="grid grid-cols-4 gap-2 rounded-lg border border-slate-800 bg-slate-900/40 p-3 text-center"
309
+ data-testid="operator-live"
310
+ >
311
+ <div>
312
+ <p class="text-lg font-semibold text-sky-400">{{ view.live.running }}</p>
313
+ <p class="text-[11px] text-slate-500">
314
+ {{ t('platformObservability.outcomes.running') }}
315
+ </p>
316
+ </div>
317
+ <div>
318
+ <p class="text-lg font-semibold text-amber-400">{{ view.live.blocked }}</p>
319
+ <p class="text-[11px] text-slate-500">
320
+ {{ t('platformObservability.outcomes.blocked') }}
321
+ </p>
322
+ </div>
323
+ <div>
324
+ <p class="text-lg font-semibold text-slate-300">{{ view.live.paused }}</p>
325
+ <p class="text-[11px] text-slate-500">
326
+ {{ t('platformObservability.outcomes.paused') }}
327
+ </p>
328
+ </div>
329
+ <div>
330
+ <p class="text-lg font-semibold text-slate-300">{{ view.live.pending }}</p>
331
+ <p class="text-[11px] text-slate-500">
332
+ {{ t('platformObservability.outcomes.pending') }}
333
+ </p>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ <div>
338
+ <h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
339
+ {{ t('platformObservability.durations.title') }}
340
+ </h2>
341
+ <div class="rounded-lg border border-slate-800 bg-slate-900/40 p-3 text-sm">
342
+ <div
343
+ v-if="view.durations.count === 0"
344
+ class="py-2 text-center text-xs text-slate-500"
345
+ >
346
+ {{ t('platformObservability.durations.empty') }}
347
+ </div>
348
+ <dl v-else class="flex items-center justify-between gap-2 text-center">
349
+ <div class="flex-1">
350
+ <dt class="text-[11px] text-slate-500">
351
+ {{ t('platformObservability.durations.avg') }}
352
+ </dt>
353
+ <dd class="font-semibold text-white">
354
+ {{ view.durations.avgMs == null ? '—' : formatMs(view.durations.avgMs) }}
355
+ </dd>
356
+ </div>
357
+ <div class="flex-1">
358
+ <dt class="text-[11px] text-slate-500">
359
+ {{ t('platformObservability.durations.min') }}
360
+ </dt>
361
+ <dd class="font-semibold text-slate-300">
362
+ {{ view.durations.minMs == null ? '—' : formatMs(view.durations.minMs) }}
363
+ </dd>
364
+ </div>
365
+ <div class="flex-1">
366
+ <dt class="text-[11px] text-slate-500">
367
+ {{ t('platformObservability.durations.max') }}
368
+ </dt>
369
+ <dd class="font-semibold text-slate-300">
370
+ {{ view.durations.maxMs == null ? '—' : formatMs(view.durations.maxMs) }}
371
+ </dd>
372
+ </div>
373
+ </dl>
374
+ </div>
375
+ </div>
376
+ </section>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ </div>
381
+ </Transition>
382
+ </Teleport>
383
+ </template>
384
+
385
+ <style scoped>
386
+ .obs-fade-enter-active,
387
+ .obs-fade-leave-active {
388
+ transition: opacity 0.15s ease;
389
+ }
390
+ .obs-fade-enter-from,
391
+ .obs-fade-leave-to {
392
+ opacity: 0;
393
+ }
394
+ </style>
@@ -0,0 +1,18 @@
1
+ import { getPlatformObservabilityContract } from '@cat-factory/contracts'
2
+ import type { PlatformObservabilityWindow } from '~/types/execution'
3
+ import type { ApiContext } from './context'
4
+
5
+ /**
6
+ * Platform-operator observability: the deployment-level aggregate health of an
7
+ * account's runs over a time window (admin-gated). The dual of `executionApi`'s
8
+ * per-run `getLlmMetrics` — account-scoped, not workspace-scoped.
9
+ */
10
+ export function platformObservabilityApi({ send }: ApiContext) {
11
+ return {
12
+ getPlatformObservability: (accountId: string, window: PlatformObservabilityWindow) =>
13
+ send(getPlatformObservabilityContract, {
14
+ pathParams: { accountId },
15
+ queryParams: { window },
16
+ }),
17
+ }
18
+ }
@@ -2,6 +2,7 @@ import type { FragmentOwnerKind } from '~/types/domain'
2
2
  import { createApiClient, createSend, createSendWith } from './api/client'
3
3
  import type { ApiContext } from './api/context'
4
4
  import { accountsApi } from './api/accounts'
5
+ import { platformObservabilityApi } from './api/platformObservability'
5
6
  import { authApi } from './api/auth'
6
7
  import { bootstrapApi } from './api/bootstrap'
7
8
  import { boardApi } from './api/board'
@@ -104,6 +105,7 @@ export function useApi() {
104
105
  ...fragmentsApi(ctx),
105
106
  ...modelsApi(ctx),
106
107
  ...accountsApi(ctx),
108
+ ...platformObservabilityApi(ctx),
107
109
  ...workspacesApi(ctx),
108
110
  ...boardApi(ctx),
109
111
  ...executionApi(ctx),
@@ -30,6 +30,9 @@ import KeyboardShortcutsHelp from '~/components/common/KeyboardShortcutsHelp.vue
30
30
  const ObservabilityPanel = defineAsyncComponent(
31
31
  () => import('~/components/panels/ObservabilityPanel.vue'),
32
32
  )
33
+ const OperatorDashboardPanel = defineAsyncComponent(
34
+ () => import('~/components/panels/OperatorDashboardPanel.vue'),
35
+ )
33
36
  const KaizenPanel = defineAsyncComponent(() => import('~/components/kaizen/KaizenPanel.vue'))
34
37
  // Occasional, externally store-gated surfaces — deferred to their own chunks like the
35
38
  // sibling document modals above. Each mounts only while its ui open-flag is set, so it
@@ -386,6 +389,7 @@ watch(
386
389
  <TaskImportModal v-if="ui.taskImport" />
387
390
  <RecurringPipelineModal v-if="ui.addRecurringFrameId" />
388
391
  <ObservabilityPanel v-if="ui.observabilityInstanceId" />
392
+ <OperatorDashboardPanel v-if="ui.operatorDashboardOpen" />
389
393
  <KaizenPanel v-if="ui.kaizenScreenOpen" />
390
394
  <DocumentSourceConnectModal v-if="ui.documentConnect" />
391
395
  <DocumentImportModal v-if="ui.documentImport" />
@@ -0,0 +1,49 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { PlatformObservability, PlatformObservabilityWindow } from '~/types/execution'
4
+ import { useAccountsStore } from '~/stores/accounts'
5
+
6
+ /**
7
+ * Platform-operator observability: the deployment-level aggregate health of the active
8
+ * account's runs (outcomes, failure taxonomy, live/parked depth, duration + trend) over a
9
+ * time window. The account-scoped, admin-gated counterpart of the per-run `observability`
10
+ * store — loaded on demand when the operator dashboard opens and re-loaded when the window
11
+ * changes. Nothing is pushed live (these are periodic rollups); a manual refresh re-fetches.
12
+ */
13
+ export const usePlatformObservabilityStore = defineStore('platformObservability', () => {
14
+ const api = useApi()
15
+ const accounts = useAccountsStore()
16
+
17
+ const window = ref<PlatformObservabilityWindow>('24h')
18
+ const view = ref<PlatformObservability | null>(null)
19
+ const loading = ref(false)
20
+ const error = ref<string | null>(null)
21
+
22
+ const accountId = computed(() => accounts.activeAccount?.id ?? null)
23
+
24
+ async function load(nextWindow?: PlatformObservabilityWindow) {
25
+ if (nextWindow) window.value = nextWindow
26
+ const id = accountId.value
27
+ if (!id) {
28
+ view.value = null
29
+ return
30
+ }
31
+ loading.value = true
32
+ error.value = null
33
+ try {
34
+ view.value = await api.getPlatformObservability(id, window.value)
35
+ } catch (err) {
36
+ error.value = err instanceof Error ? err.message : 'Failed to load platform observability'
37
+ } finally {
38
+ loading.value = false
39
+ }
40
+ }
41
+
42
+ /** Switch the window and reload. */
43
+ async function setWindow(next: PlatformObservabilityWindow) {
44
+ if (next === window.value && view.value) return
45
+ await load(next)
46
+ }
47
+
48
+ return { window, view, loading, error, accountId, load, setWindow }
49
+ })
@@ -167,6 +167,11 @@ export function createUiModals() {
167
167
  // today, pluggable). NB: distinct from `observabilityInstanceId`, which is the
168
168
  // LLM per-call observability panel (see the result-views slice).
169
169
  const observabilityConnectionOpen = ref(false)
170
+ // Platform-operator observability: the deployment-level dashboard (aggregate run health of
171
+ // the account — outcomes, failure taxonomy, live depth, durations). Admin-gated. Distinct
172
+ // from `observabilityConnectionOpen` (the Datadog connection) AND `observabilityInstanceId`
173
+ // (the per-run LLM call panel).
174
+ const operatorDashboardOpen = ref(false)
170
175
  // Private package registries: the workspace's npm/GitHub-Packages entries agent
171
176
  // containers install with. Opened from the Integrations hub.
172
177
  const packageRegistriesOpen = ref(false)
@@ -497,6 +502,13 @@ export function createUiModals() {
497
502
  function closeObservabilityConnection() {
498
503
  observabilityConnectionOpen.value = false
499
504
  }
505
+ function openOperatorDashboard() {
506
+ resetHubReturn()
507
+ operatorDashboardOpen.value = true
508
+ }
509
+ function closeOperatorDashboard() {
510
+ operatorDashboardOpen.value = false
511
+ }
500
512
  function openPackageRegistries() {
501
513
  resetHubReturn()
502
514
  packageRegistriesOpen.value = true
@@ -690,6 +702,7 @@ export function createUiModals() {
690
702
  accountSettingsTab,
691
703
  accountSettingsScrollTarget,
692
704
  observabilityConnectionOpen,
705
+ operatorDashboardOpen,
693
706
  packageRegistriesOpen,
694
707
  apiTokensOpen,
695
708
  infrastructureOpen,
@@ -778,6 +791,8 @@ export function createUiModals() {
778
791
  setAccountSettingsTab,
779
792
  openObservabilityConnection,
780
793
  closeObservabilityConnection,
794
+ openOperatorDashboard,
795
+ closeOperatorDashboard,
781
796
  openPackageRegistries,
782
797
  closePackageRegistries,
783
798
  openApiTokens,
@@ -25,6 +25,11 @@ export type {
25
25
  LlmCallActivity,
26
26
  LlmExportInsight,
27
27
  LlmMetricsExport,
28
+ PlatformObservability,
29
+ PlatformObservabilityWindow,
30
+ PlatformOutcomeTotals,
31
+ PlatformTrendPoint,
32
+ PlatformFailureSlice,
28
33
  AgentSearchQuery,
29
34
  WebSearchAvailability,
30
35
  WebSearchProvider,
@@ -2745,6 +2745,64 @@
2745
2745
  "queriesTitle": "Durchgeführte Suchen"
2746
2746
  }
2747
2747
  },
2748
+ "platformObservability": {
2749
+ "title": "Plattform-Observability",
2750
+ "refresh": "Aktualisieren",
2751
+ "close": "Schließen",
2752
+ "loading": "Plattformzustand wird geladen…",
2753
+ "retry": "Erneut versuchen",
2754
+ "window": {
2755
+ "oneHour": "Letzte Stunde",
2756
+ "oneDay": "Letzte 24 Stunden",
2757
+ "sevenDays": "Letzte 7 Tage"
2758
+ },
2759
+ "outcomes": {
2760
+ "title": "Lauf-Ergebnisse",
2761
+ "total": "Läufe gesamt",
2762
+ "done": "Abgeschlossen",
2763
+ "failed": "Fehlgeschlagen",
2764
+ "successRate": "Erfolgsquote",
2765
+ "running": "Läuft",
2766
+ "blocked": "Blockiert",
2767
+ "paused": "Pausiert",
2768
+ "pending": "Ausstehend"
2769
+ },
2770
+ "trend": {
2771
+ "title": "Ergebnisverlauf",
2772
+ "done": "Abgeschlossen",
2773
+ "failed": "Fehlgeschlagen",
2774
+ "other": "Sonstige",
2775
+ "empty": "Keine Läufe in diesem Zeitraum."
2776
+ },
2777
+ "failures": {
2778
+ "title": "Fehleraufschlüsselung",
2779
+ "empty": "Keine Fehler in diesem Zeitraum."
2780
+ },
2781
+ "failureKind": {
2782
+ "preflight": "Vorprüfung",
2783
+ "dispatch": "Zustellung",
2784
+ "environment": "Umgebung",
2785
+ "evicted": "Verdrängt",
2786
+ "timeout": "Zeitüberschreitung",
2787
+ "agent": "Agent",
2788
+ "job_failed": "Job fehlgeschlagen",
2789
+ "rejected": "Abgelehnt",
2790
+ "companion_rejected": "Begleiter abgelehnt",
2791
+ "stalled": "Hängengeblieben",
2792
+ "cancelled": "Abgebrochen",
2793
+ "unknown": "Unbekannt"
2794
+ },
2795
+ "durations": {
2796
+ "title": "Laufdauer",
2797
+ "avg": "Durchschnitt",
2798
+ "min": "Min.",
2799
+ "max": "Max.",
2800
+ "empty": "Keine abgeschlossenen Läufe in diesem Zeitraum."
2801
+ },
2802
+ "live": {
2803
+ "title": "Jetzt aktiv"
2804
+ }
2805
+ },
2748
2806
  "auth": {
2749
2807
  "gate": {
2750
2808
  "loading": "Wird geladen…"
@@ -3849,6 +3907,7 @@
3849
3907
  "workspaceSettings": "Workspace-Einstellungen",
3850
3908
  "modelConfiguration": "Modellkonfiguration",
3851
3909
  "accountSettings": "Kontoeinstellungen",
3910
+ "operatorDashboard": "Plattform-Observability",
3852
3911
  "environmentSetup": "Umgebungseinrichtung"
3853
3912
  },
3854
3913
  "errors": {