@cat-factory/app 0.161.0 → 0.162.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/app/components/panels/ReportsPanel.logic.spec.ts +76 -0
- package/app/components/panels/ReportsPanel.logic.ts +71 -0
- package/app/components/panels/ReportsPanel.vue +471 -0
- package/app/components/panels/ReportsSpendBreakdown.vue +70 -0
- package/app/composables/api/reports.ts +19 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/useNavContributions.ts +1 -0
- package/app/modular/nav-contributions.spec.ts +1 -0
- package/app/modular/nav-contributions.ts +11 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/reports.spec.ts +113 -0
- package/app/stores/reports.ts +91 -0
- package/app/stores/ui/modals.ts +14 -0
- package/app/types/execution.ts +8 -0
- package/i18n/locales/de.json +65 -0
- package/i18n/locales/en.json +65 -0
- package/i18n/locales/es.json +65 -0
- package/i18n/locales/fr.json +65 -0
- package/i18n/locales/he.json +65 -0
- package/i18n/locales/it.json +65 -0
- package/i18n/locales/ja.json +65 -0
- package/i18n/locales/pl.json +65 -0
- package/i18n/locales/tr.json +65 -0
- package/i18n/locales/uk.json +65 -0
- package/package.json +2 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { ReportSpendRow } from '~/types/execution'
|
|
4
|
+
import { maxOf, segmentPct, spendMagnitude } from './ReportsPanel.logic'
|
|
5
|
+
|
|
6
|
+
// One ranked spend breakdown: a horizontal bar per slice, split into the metered
|
|
7
|
+
// (`violet-500`, real money) and subscription (`amber-600`, illustrative equivalent-API
|
|
8
|
+
// cost) segments with a surface gap between them. Extracted from `ReportsPanel.vue` because
|
|
9
|
+
// the panel renders four of these against different dimensions — the shape is identical,
|
|
10
|
+
// only the row list and the heading differ.
|
|
11
|
+
//
|
|
12
|
+
// Every bar is scaled against the HEAVIEST slice in this list, so a full bar means "the
|
|
13
|
+
// biggest consumer here", never an absolute budget. The panel owns the legend (the two
|
|
14
|
+
// series mean the same thing in every breakdown, so repeating it per card would be noise).
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
rows: ReportSpendRow[]
|
|
17
|
+
currency: string
|
|
18
|
+
testId: string
|
|
19
|
+
/** Resolves a slice's display name (the panel owns the unattributed/i18n vocabulary). */
|
|
20
|
+
labelOf: (row: ReportSpendRow) => string
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const { t, n } = useI18n()
|
|
24
|
+
const money = (value: number) => n(value, { key: 'currency', currency: props.currency })
|
|
25
|
+
const max = computed(() => maxOf(props.rows, spendMagnitude))
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div class="rounded-lg border border-slate-800 bg-slate-900/40 p-4">
|
|
30
|
+
<div v-if="!rows.length" class="py-4 text-center text-xs text-slate-500">
|
|
31
|
+
{{ t('reports.spend.empty') }}
|
|
32
|
+
</div>
|
|
33
|
+
<ul v-else class="flex flex-col gap-2.5" :data-testid="testId">
|
|
34
|
+
<li v-for="row in rows" :key="row.key" class="text-xs" data-testid="reports-spend-row">
|
|
35
|
+
<div class="mb-1 flex items-baseline justify-between gap-2">
|
|
36
|
+
<span class="min-w-0 truncate text-slate-300">{{ labelOf(row) }}</span>
|
|
37
|
+
<!-- The metered figure alone is the slice's SPEND. The subscription cost rides
|
|
38
|
+
beside it, in its own series colour and explicitly named, because it is the
|
|
39
|
+
illustrative cost of flat-rate quota usage — adding the two into one currency
|
|
40
|
+
figure would report money that was never billed. -->
|
|
41
|
+
<span class="shrink-0 tabular-nums text-slate-400">
|
|
42
|
+
{{ money(row.meteredCost) }}
|
|
43
|
+
<span v-if="row.subscriptionCost > 0" class="text-amber-400">
|
|
44
|
+
{{ t('reports.spend.subscriptionAside', { value: money(row.subscriptionCost) }) }}
|
|
45
|
+
</span>
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="flex h-1.5 gap-[2px]">
|
|
49
|
+
<div
|
|
50
|
+
class="h-1.5 rounded-full bg-violet-500"
|
|
51
|
+
:style="{ width: `${segmentPct(row.meteredCost, max)}%` }"
|
|
52
|
+
/>
|
|
53
|
+
<div
|
|
54
|
+
class="h-1.5 rounded-full bg-amber-600"
|
|
55
|
+
:style="{ width: `${segmentPct(row.subscriptionCost, max)}%` }"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<p class="mt-1 text-[10px] text-slate-500">
|
|
59
|
+
{{ t('reports.spend.calls', { count: row.calls }, row.calls) }} ·
|
|
60
|
+
{{
|
|
61
|
+
t('reports.spend.tokens', {
|
|
62
|
+
input: formatTokens(row.inputTokens),
|
|
63
|
+
output: formatTokens(row.outputTokens),
|
|
64
|
+
})
|
|
65
|
+
}}
|
|
66
|
+
</p>
|
|
67
|
+
</li>
|
|
68
|
+
</ul>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getReportsContract } from '@cat-factory/contracts'
|
|
2
|
+
import type { ReportWindow } from '~/types/execution'
|
|
3
|
+
import type { ApiContext } from './context'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reports: cross-cutting usage analytics for an account over a time window
|
|
7
|
+
* (admin-gated). The sibling of `platformObservabilityApi` — same account scope,
|
|
8
|
+
* a different question ("where did the spend and the work go" vs "is it healthy").
|
|
9
|
+
* `workspaceId` narrows every breakdown to one board.
|
|
10
|
+
*/
|
|
11
|
+
export function reportsApi({ send }: ApiContext) {
|
|
12
|
+
return {
|
|
13
|
+
getReports: (accountId: string, window: ReportWindow, workspaceId?: string | null) =>
|
|
14
|
+
send(getReportsContract, {
|
|
15
|
+
pathParams: { accountId },
|
|
16
|
+
queryParams: { window, ...(workspaceId ? { workspaceId } : {}) },
|
|
17
|
+
}),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -3,6 +3,7 @@ import { createApiClient, createSend, createSendWith } from './api/client'
|
|
|
3
3
|
import type { ApiContext } from './api/context'
|
|
4
4
|
import { accountsApi } from './api/accounts'
|
|
5
5
|
import { platformObservabilityApi } from './api/platformObservability'
|
|
6
|
+
import { reportsApi } from './api/reports'
|
|
6
7
|
import { authApi } from './api/auth'
|
|
7
8
|
import { bootstrapApi } from './api/bootstrap'
|
|
8
9
|
import { boardApi } from './api/board'
|
|
@@ -112,6 +113,7 @@ export function useApi() {
|
|
|
112
113
|
...modelsApi(ctx),
|
|
113
114
|
...accountsApi(ctx),
|
|
114
115
|
...platformObservabilityApi(ctx),
|
|
116
|
+
...reportsApi(ctx),
|
|
115
117
|
...workspacesApi(ctx),
|
|
116
118
|
...boardApi(ctx),
|
|
117
119
|
...executionApi(ctx),
|
|
@@ -45,6 +45,7 @@ export function useNavContributions() {
|
|
|
45
45
|
localModels: () => ui.openLocalModels(),
|
|
46
46
|
accountSettings: () => ui.openAccountSettings(),
|
|
47
47
|
operatorDashboard: () => ui.openOperatorDashboard(),
|
|
48
|
+
reports: () => ui.openReports(),
|
|
48
49
|
shortcuts: () => ui.openShortcutsHelp(),
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -96,6 +96,7 @@ export const NAV_ACTIONS = [
|
|
|
96
96
|
'localModels',
|
|
97
97
|
'accountSettings',
|
|
98
98
|
'operatorDashboard',
|
|
99
|
+
'reports',
|
|
99
100
|
'shortcuts',
|
|
100
101
|
] as const
|
|
101
102
|
|
|
@@ -354,6 +355,16 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
354
355
|
testId: 'nav-operator-dashboard',
|
|
355
356
|
sidebar: { group: 'configuration', order: 40 },
|
|
356
357
|
},
|
|
358
|
+
{
|
|
359
|
+
id: 'reports',
|
|
360
|
+
labelKey: 'nav.reports',
|
|
361
|
+
icon: 'i-lucide-chart-column',
|
|
362
|
+
surfaces: S('sidebar'),
|
|
363
|
+
gate: (g) => g.accountsEnabled && g.isAccountAdmin,
|
|
364
|
+
action: 'reports',
|
|
365
|
+
testId: 'nav-reports',
|
|
366
|
+
sidebar: { group: 'configuration', order: 45 },
|
|
367
|
+
},
|
|
357
368
|
{
|
|
358
369
|
id: 'keyboard-shortcuts',
|
|
359
370
|
labelKey: 'layout.commandBar.cmd.shortcuts',
|
package/app/pages/index.vue
CHANGED
|
@@ -35,6 +35,7 @@ const ObservabilityPanel = defineAsyncComponent(
|
|
|
35
35
|
const OperatorDashboardPanel = defineAsyncComponent(
|
|
36
36
|
() => import('~/components/panels/OperatorDashboardPanel.vue'),
|
|
37
37
|
)
|
|
38
|
+
const ReportsPanel = defineAsyncComponent(() => import('~/components/panels/ReportsPanel.vue'))
|
|
38
39
|
const KaizenPanel = defineAsyncComponent(() => import('~/components/kaizen/KaizenPanel.vue'))
|
|
39
40
|
// Occasional, externally store-gated surfaces — deferred to their own chunks like the
|
|
40
41
|
// sibling document modals above. Each mounts only while its ui open-flag is set, so it
|
|
@@ -400,6 +401,7 @@ watch(
|
|
|
400
401
|
<RecurringPipelineModal v-if="ui.addRecurringFrameId" />
|
|
401
402
|
<ObservabilityPanel v-if="ui.observabilityInstanceId" />
|
|
402
403
|
<OperatorDashboardPanel v-if="ui.operatorDashboardOpen" />
|
|
404
|
+
<ReportsPanel v-if="ui.reportsOpen" />
|
|
403
405
|
<KaizenPanel v-if="ui.kaizenScreenOpen" />
|
|
404
406
|
<DocumentSourceConnectModal v-if="ui.documentConnect" />
|
|
405
407
|
<DocumentImportModal v-if="ui.documentImport" />
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { useAccountsStore } from '~/stores/accounts'
|
|
3
|
+
import { useReportsStore } from '~/stores/reports'
|
|
4
|
+
import type { ReportsView } from '~/types/execution'
|
|
5
|
+
|
|
6
|
+
// The reports view is loaded three ways that can each be triggered while another is still in
|
|
7
|
+
// flight — the window buttons, the board filter and the refresh button — so the store's
|
|
8
|
+
// monotonicity guard is the thing that keeps the panel from showing numbers for a window the
|
|
9
|
+
// user already left. These drive that race directly.
|
|
10
|
+
|
|
11
|
+
/** Minimal projection — only what the store stores and these assertions read. */
|
|
12
|
+
function view(over: Partial<ReportsView> = {}): ReportsView {
|
|
13
|
+
return {
|
|
14
|
+
window: '7d',
|
|
15
|
+
generatedAt: 1_000,
|
|
16
|
+
since: 0,
|
|
17
|
+
workspaceId: null,
|
|
18
|
+
currency: 'EUR',
|
|
19
|
+
totals: { inputTokens: 0, outputTokens: 0, calls: 0, meteredCost: 0, subscriptionCost: 0 },
|
|
20
|
+
spend: { byModel: [], byAgentKind: [], byWorkspace: [], byService: [], byTaskType: [] },
|
|
21
|
+
activity: { byWorkspace: [], byService: [], byTaskType: [] },
|
|
22
|
+
trend: { bucketMs: 1_000, points: [] },
|
|
23
|
+
...over,
|
|
24
|
+
} as ReportsView
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Seed an active account, since every load is scoped to one. */
|
|
28
|
+
function seedAccount() {
|
|
29
|
+
const accounts = useAccountsStore()
|
|
30
|
+
accounts.accounts = [
|
|
31
|
+
{
|
|
32
|
+
id: 'acc1',
|
|
33
|
+
type: 'org',
|
|
34
|
+
name: 'Acme',
|
|
35
|
+
githubAccountLogin: null,
|
|
36
|
+
createdAt: 0,
|
|
37
|
+
roles: null,
|
|
38
|
+
},
|
|
39
|
+
] as never
|
|
40
|
+
accounts.activeAccountId = 'acc1'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('reports store — concurrent loads', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
seedAccount()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('a stale load never overwrites a fresher one that already resolved', async () => {
|
|
49
|
+
// The user picks 24h, then immediately 90d. The 24h request is slower and lands LAST;
|
|
50
|
+
// committing it would leave the panel labelled 90d while showing 24h numbers.
|
|
51
|
+
let resolveSlow!: (v: ReportsView) => void
|
|
52
|
+
const slow = new Promise<ReportsView>((res) => {
|
|
53
|
+
resolveSlow = res
|
|
54
|
+
})
|
|
55
|
+
const responses: Record<string, Promise<ReportsView>> = {
|
|
56
|
+
'24h': slow,
|
|
57
|
+
'90d': Promise.resolve(view({ window: '90d', totals: { ...view().totals, calls: 42 } })),
|
|
58
|
+
}
|
|
59
|
+
vi.stubGlobal('useApi', () => ({
|
|
60
|
+
getReports: (_id: string, window: string) => responses[window]!,
|
|
61
|
+
}))
|
|
62
|
+
const store = useReportsStore()
|
|
63
|
+
|
|
64
|
+
const first = store.setWindow('24h')
|
|
65
|
+
const second = store.setWindow('90d')
|
|
66
|
+
await second
|
|
67
|
+
resolveSlow(view({ window: '24h', totals: { ...view().totals, calls: 7 } }))
|
|
68
|
+
await first
|
|
69
|
+
|
|
70
|
+
expect(store.view?.window).toBe('90d')
|
|
71
|
+
expect(store.view?.totals.calls).toBe(42)
|
|
72
|
+
// The superseded load must not resurrect the spinner it started either.
|
|
73
|
+
expect(store.loading).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('a superseded FAILURE never replaces the newer view with an error', async () => {
|
|
77
|
+
// The same race the other way round: an in-flight load rejects after a later
|
|
78
|
+
// one succeeded. Committing it would blank a perfectly good panel.
|
|
79
|
+
let rejectSlow!: (e: Error) => void
|
|
80
|
+
const slow = new Promise<ReportsView>((_res, rej) => {
|
|
81
|
+
rejectSlow = rej
|
|
82
|
+
})
|
|
83
|
+
const responses: Record<string, Promise<ReportsView>> = {
|
|
84
|
+
'24h': slow,
|
|
85
|
+
'90d': Promise.resolve(view({ window: '90d' })),
|
|
86
|
+
}
|
|
87
|
+
vi.stubGlobal('useApi', () => ({
|
|
88
|
+
getReports: (_id: string, window: string) => responses[window]!,
|
|
89
|
+
}))
|
|
90
|
+
const store = useReportsStore()
|
|
91
|
+
|
|
92
|
+
const first = store.setWindow('24h')
|
|
93
|
+
await store.setWindow('90d')
|
|
94
|
+
rejectSlow(new Error('gateway timeout'))
|
|
95
|
+
await first
|
|
96
|
+
|
|
97
|
+
expect(store.failed).toBe(false)
|
|
98
|
+
expect(store.error).toBeNull()
|
|
99
|
+
expect(store.view?.window).toBe('90d')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('records a failure from the newest load, with the raw message as detail', async () => {
|
|
103
|
+
vi.stubGlobal('useApi', () => ({
|
|
104
|
+
getReports: () => Promise.reject(new Error('reports are not available')),
|
|
105
|
+
}))
|
|
106
|
+
const store = useReportsStore()
|
|
107
|
+
await store.load()
|
|
108
|
+
expect(store.failed).toBe(true)
|
|
109
|
+
// Raw backend prose only — the localized heading is the panel's job.
|
|
110
|
+
expect(store.error).toBe('reports are not available')
|
|
111
|
+
expect(store.loading).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { ReportWindow, ReportsView } from '~/types/execution'
|
|
4
|
+
import { useAccountsStore } from '~/stores/accounts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reports: cross-cutting usage analytics for the active account — spend per model and
|
|
8
|
+
* agent kind, spend + run activity per workspace / service / task type, and a spend trend,
|
|
9
|
+
* over a selectable window and optionally narrowed to one board.
|
|
10
|
+
*
|
|
11
|
+
* The sibling of the `platformObservability` store: same account scope, same admin gate,
|
|
12
|
+
* same on-demand load. Nothing is pushed live (these are periodic rollups); changing the
|
|
13
|
+
* window or the board filter re-fetches, and a manual refresh re-fetches unconditionally.
|
|
14
|
+
*/
|
|
15
|
+
export const useReportsStore = defineStore('reports', () => {
|
|
16
|
+
const api = useApi()
|
|
17
|
+
const accounts = useAccountsStore()
|
|
18
|
+
|
|
19
|
+
const window = ref<ReportWindow>('7d')
|
|
20
|
+
/** The single board every breakdown is narrowed to, or null for the whole account. */
|
|
21
|
+
const workspaceFilter = ref<string | null>(null)
|
|
22
|
+
const view = ref<ReportsView | null>(null)
|
|
23
|
+
const loading = ref(false)
|
|
24
|
+
/** Whether the last load failed. The panel owns the localized wording. */
|
|
25
|
+
const failed = ref(false)
|
|
26
|
+
/**
|
|
27
|
+
* The backend's raw (untranslated) message for a failed load, shown beneath the localized
|
|
28
|
+
* heading as a last-resort detail — null when the failure carried no message at all.
|
|
29
|
+
*/
|
|
30
|
+
const error = ref<string | null>(null)
|
|
31
|
+
|
|
32
|
+
const accountId = computed(() => accounts.activeAccount?.id ?? null)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Monotonicity guard. The window buttons, the board filter and the refresh button can each
|
|
36
|
+
* start a load, so two are easily in flight at once — and without this a staler response
|
|
37
|
+
* resolving later would overwrite the newer view (and its `loading`/error state) with
|
|
38
|
+
* numbers for a window the user already moved off. Only the newest load may commit.
|
|
39
|
+
*/
|
|
40
|
+
let latest = 0
|
|
41
|
+
|
|
42
|
+
async function load() {
|
|
43
|
+
const id = accountId.value
|
|
44
|
+
if (!id) {
|
|
45
|
+
view.value = null
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
const seq = ++latest
|
|
49
|
+
loading.value = true
|
|
50
|
+
failed.value = false
|
|
51
|
+
error.value = null
|
|
52
|
+
try {
|
|
53
|
+
const next = await api.getReports(id, window.value, workspaceFilter.value)
|
|
54
|
+
if (seq !== latest) return
|
|
55
|
+
view.value = next
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (seq !== latest) return
|
|
58
|
+
failed.value = true
|
|
59
|
+
error.value = err instanceof Error ? err.message : null
|
|
60
|
+
} finally {
|
|
61
|
+
if (seq === latest) loading.value = false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Switch the window and reload (a no-op when it is already the loaded one). */
|
|
66
|
+
async function setWindow(next: ReportWindow) {
|
|
67
|
+
if (next === window.value && view.value) return
|
|
68
|
+
window.value = next
|
|
69
|
+
await load()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Narrow to one board (null = the whole account) and reload. */
|
|
73
|
+
async function setWorkspaceFilter(next: string | null) {
|
|
74
|
+
if (next === workspaceFilter.value && view.value) return
|
|
75
|
+
workspaceFilter.value = next
|
|
76
|
+
await load()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
window,
|
|
81
|
+
workspaceFilter,
|
|
82
|
+
view,
|
|
83
|
+
loading,
|
|
84
|
+
failed,
|
|
85
|
+
error,
|
|
86
|
+
accountId,
|
|
87
|
+
load,
|
|
88
|
+
setWindow,
|
|
89
|
+
setWorkspaceFilter,
|
|
90
|
+
}
|
|
91
|
+
})
|
package/app/stores/ui/modals.ts
CHANGED
|
@@ -440,6 +440,10 @@ function createIntegrationPanelModals(resetHubReturn: ResetHubReturn) {
|
|
|
440
440
|
// from `observabilityConnectionOpen` (the Datadog connection) AND `observabilityInstanceId`
|
|
441
441
|
// (the per-run LLM call panel).
|
|
442
442
|
const operatorDashboardOpen = ref(false)
|
|
443
|
+
// Reports: the cross-cutting usage-analytics panel over the same account scope (spend per
|
|
444
|
+
// model / agent kind, spend + activity per workspace / service / task type). Admin-gated.
|
|
445
|
+
// Distinct from `operatorDashboardOpen`, which answers the deployment-HEALTH question.
|
|
446
|
+
const reportsOpen = ref(false)
|
|
443
447
|
// Private package registries: the workspace's npm/GitHub-Packages entries agent
|
|
444
448
|
// containers install with. Opened from the Integrations hub.
|
|
445
449
|
const packageRegistriesOpen = ref(false)
|
|
@@ -486,6 +490,13 @@ function createIntegrationPanelModals(resetHubReturn: ResetHubReturn) {
|
|
|
486
490
|
function closeOperatorDashboard() {
|
|
487
491
|
operatorDashboardOpen.value = false
|
|
488
492
|
}
|
|
493
|
+
function openReports() {
|
|
494
|
+
resetHubReturn()
|
|
495
|
+
reportsOpen.value = true
|
|
496
|
+
}
|
|
497
|
+
function closeReports() {
|
|
498
|
+
reportsOpen.value = false
|
|
499
|
+
}
|
|
489
500
|
function openPackageRegistries() {
|
|
490
501
|
resetHubReturn()
|
|
491
502
|
packageRegistriesOpen.value = true
|
|
@@ -544,6 +555,7 @@ function createIntegrationPanelModals(resetHubReturn: ResetHubReturn) {
|
|
|
544
555
|
slackOpen,
|
|
545
556
|
observabilityConnectionOpen,
|
|
546
557
|
operatorDashboardOpen,
|
|
558
|
+
reportsOpen,
|
|
547
559
|
packageRegistriesOpen,
|
|
548
560
|
apiTokensOpen,
|
|
549
561
|
modelConfigOpen,
|
|
@@ -559,6 +571,8 @@ function createIntegrationPanelModals(resetHubReturn: ResetHubReturn) {
|
|
|
559
571
|
openObservabilityConnection,
|
|
560
572
|
closeObservabilityConnection,
|
|
561
573
|
openOperatorDashboard,
|
|
574
|
+
openReports,
|
|
575
|
+
closeReports,
|
|
562
576
|
closeOperatorDashboard,
|
|
563
577
|
openPackageRegistries,
|
|
564
578
|
closePackageRegistries,
|
package/app/types/execution.ts
CHANGED
|
@@ -30,6 +30,14 @@ export type {
|
|
|
30
30
|
PlatformOutcomeTotals,
|
|
31
31
|
PlatformTrendPoint,
|
|
32
32
|
PlatformFailureSlice,
|
|
33
|
+
ReportWindow,
|
|
34
|
+
ReportSpendDimension,
|
|
35
|
+
ReportActivityDimension,
|
|
36
|
+
ReportSpendRow,
|
|
37
|
+
ReportActivityRow,
|
|
38
|
+
ReportTrendPoint,
|
|
39
|
+
ReportTotals,
|
|
40
|
+
ReportsView,
|
|
33
41
|
AgentSearchQuery,
|
|
34
42
|
WebSearchAvailability,
|
|
35
43
|
WebSearchProvider,
|
package/i18n/locales/de.json
CHANGED
|
@@ -3000,6 +3000,70 @@
|
|
|
3000
3000
|
"title": "Jetzt aktiv"
|
|
3001
3001
|
}
|
|
3002
3002
|
},
|
|
3003
|
+
"reports": {
|
|
3004
|
+
"title": "Berichte",
|
|
3005
|
+
"refresh": "Aktualisieren",
|
|
3006
|
+
"loading": "Berichte werden geladen…",
|
|
3007
|
+
"retry": "Erneut versuchen",
|
|
3008
|
+
"error": "Berichte konnten nicht geladen werden.",
|
|
3009
|
+
"period": "{from} bis {to}",
|
|
3010
|
+
"unattributed": "Nicht zugeordnet",
|
|
3011
|
+
"window": {
|
|
3012
|
+
"oneDay": "Letzte 24 Stunden",
|
|
3013
|
+
"sevenDays": "Letzte 7 Tage",
|
|
3014
|
+
"thirtyDays": "Letzte 30 Tage",
|
|
3015
|
+
"ninetyDays": "Letzte 90 Tage"
|
|
3016
|
+
},
|
|
3017
|
+
"filter": {
|
|
3018
|
+
"board": "Board",
|
|
3019
|
+
"allBoards": "Alle Boards"
|
|
3020
|
+
},
|
|
3021
|
+
"dimension": {
|
|
3022
|
+
"workspace": "Board",
|
|
3023
|
+
"service": "Service",
|
|
3024
|
+
"taskType": "Aufgabentyp"
|
|
3025
|
+
},
|
|
3026
|
+
"breakdown": {
|
|
3027
|
+
"title": "Aufschlüsselung nach"
|
|
3028
|
+
},
|
|
3029
|
+
"totals": {
|
|
3030
|
+
"title": "In diesem Zeitraum",
|
|
3031
|
+
"metered": "Abgerechnete Kosten",
|
|
3032
|
+
"subscription": "Abo",
|
|
3033
|
+
"calls": "LLM-Aufrufe",
|
|
3034
|
+
"tokens": "Tokens",
|
|
3035
|
+
"illustrative": "Abokosten sind nur kalkulatorisch – diese Tarife sind pauschal, werden nicht pro Token abgerechnet und zählen nie als Ausgaben."
|
|
3036
|
+
},
|
|
3037
|
+
"trend": {
|
|
3038
|
+
"title": "Kosten im Zeitverlauf",
|
|
3039
|
+
"empty": "In diesem Zeitraum wurde keine Nutzung erfasst."
|
|
3040
|
+
},
|
|
3041
|
+
"legend": {
|
|
3042
|
+
"metered": "Abgerechnet",
|
|
3043
|
+
"subscription": "Abo"
|
|
3044
|
+
},
|
|
3045
|
+
"spend": {
|
|
3046
|
+
"byModel": "Kosten nach Modell",
|
|
3047
|
+
"byAgentKind": "Kosten nach Agententyp",
|
|
3048
|
+
"heading": "Kosten",
|
|
3049
|
+
"empty": "In diesem Zeitraum wurde keine Nutzung erfasst.",
|
|
3050
|
+
"calls": "{count} Aufruf | {count} Aufrufe",
|
|
3051
|
+
"tokens": "{input} rein / {output} raus",
|
|
3052
|
+
"subscriptionAside": "+{value} Abo"
|
|
3053
|
+
},
|
|
3054
|
+
"activity": {
|
|
3055
|
+
"heading": "Läufe",
|
|
3056
|
+
"empty": "In diesem Zeitraum gab es keine Läufe.",
|
|
3057
|
+
"runs": "{count} Lauf | {count} Läufe",
|
|
3058
|
+
"avg": "Ø {value}"
|
|
3059
|
+
},
|
|
3060
|
+
"status": {
|
|
3061
|
+
"done": "Abgeschlossen",
|
|
3062
|
+
"failed": "Fehlgeschlagen",
|
|
3063
|
+
"running": "Läuft",
|
|
3064
|
+
"other": "Sonstige"
|
|
3065
|
+
}
|
|
3066
|
+
},
|
|
3003
3067
|
"auth": {
|
|
3004
3068
|
"gate": {
|
|
3005
3069
|
"loading": "Wird geladen…"
|
|
@@ -4159,6 +4223,7 @@
|
|
|
4159
4223
|
"modelConfiguration": "Modellkonfiguration",
|
|
4160
4224
|
"accountSettings": "Kontoeinstellungen",
|
|
4161
4225
|
"operatorDashboard": "Plattform-Observability",
|
|
4226
|
+
"reports": "Berichte",
|
|
4162
4227
|
"environmentSetup": "Umgebungseinrichtung"
|
|
4163
4228
|
},
|
|
4164
4229
|
"errors": {
|
package/i18n/locales/en.json
CHANGED
|
@@ -130,6 +130,7 @@
|
|
|
130
130
|
"modelConfiguration": "Model configuration",
|
|
131
131
|
"accountSettings": "Account settings",
|
|
132
132
|
"operatorDashboard": "Platform observability",
|
|
133
|
+
"reports": "Reports",
|
|
133
134
|
"environmentSetup": "Environment setup"
|
|
134
135
|
},
|
|
135
136
|
"board": {
|
|
@@ -1524,6 +1525,70 @@
|
|
|
1524
1525
|
"title": "Live now"
|
|
1525
1526
|
}
|
|
1526
1527
|
},
|
|
1528
|
+
"reports": {
|
|
1529
|
+
"title": "Reports",
|
|
1530
|
+
"refresh": "Refresh",
|
|
1531
|
+
"loading": "Loading reports…",
|
|
1532
|
+
"retry": "Retry",
|
|
1533
|
+
"error": "Reports could not be loaded.",
|
|
1534
|
+
"period": "{from} to {to}",
|
|
1535
|
+
"unattributed": "Unattributed",
|
|
1536
|
+
"window": {
|
|
1537
|
+
"oneDay": "Last 24 hours",
|
|
1538
|
+
"sevenDays": "Last 7 days",
|
|
1539
|
+
"thirtyDays": "Last 30 days",
|
|
1540
|
+
"ninetyDays": "Last 90 days"
|
|
1541
|
+
},
|
|
1542
|
+
"filter": {
|
|
1543
|
+
"board": "Board",
|
|
1544
|
+
"allBoards": "All boards"
|
|
1545
|
+
},
|
|
1546
|
+
"dimension": {
|
|
1547
|
+
"workspace": "Board",
|
|
1548
|
+
"service": "Service",
|
|
1549
|
+
"taskType": "Task type"
|
|
1550
|
+
},
|
|
1551
|
+
"breakdown": {
|
|
1552
|
+
"title": "Breakdown by"
|
|
1553
|
+
},
|
|
1554
|
+
"totals": {
|
|
1555
|
+
"title": "This window",
|
|
1556
|
+
"metered": "Metered spend",
|
|
1557
|
+
"subscription": "Subscription",
|
|
1558
|
+
"calls": "LLM calls",
|
|
1559
|
+
"tokens": "Tokens",
|
|
1560
|
+
"illustrative": "Subscription costs are illustrative — those plans are flat-rate, not billed per token, and are never counted as spend."
|
|
1561
|
+
},
|
|
1562
|
+
"trend": {
|
|
1563
|
+
"title": "Spend over time",
|
|
1564
|
+
"empty": "No recorded usage in this window."
|
|
1565
|
+
},
|
|
1566
|
+
"legend": {
|
|
1567
|
+
"metered": "Metered",
|
|
1568
|
+
"subscription": "Subscription"
|
|
1569
|
+
},
|
|
1570
|
+
"spend": {
|
|
1571
|
+
"byModel": "Spend by model",
|
|
1572
|
+
"byAgentKind": "Spend by agent kind",
|
|
1573
|
+
"heading": "Spend",
|
|
1574
|
+
"empty": "No recorded usage in this window.",
|
|
1575
|
+
"calls": "{count} call | {count} calls",
|
|
1576
|
+
"tokens": "{input} in / {output} out",
|
|
1577
|
+
"subscriptionAside": "+{value} subscription"
|
|
1578
|
+
},
|
|
1579
|
+
"activity": {
|
|
1580
|
+
"heading": "Runs",
|
|
1581
|
+
"empty": "No runs in this window.",
|
|
1582
|
+
"runs": "{count} run | {count} runs",
|
|
1583
|
+
"avg": "avg {value}"
|
|
1584
|
+
},
|
|
1585
|
+
"status": {
|
|
1586
|
+
"done": "Completed",
|
|
1587
|
+
"failed": "Failed",
|
|
1588
|
+
"running": "Running",
|
|
1589
|
+
"other": "Other"
|
|
1590
|
+
}
|
|
1591
|
+
},
|
|
1527
1592
|
"auth": {
|
|
1528
1593
|
"gate": {
|
|
1529
1594
|
"loading": "Loading…"
|
package/i18n/locales/es.json
CHANGED
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"modelConfiguration": "Configuración del modelo",
|
|
112
112
|
"accountSettings": "Ajustes de la cuenta",
|
|
113
113
|
"operatorDashboard": "Observabilidad de la plataforma",
|
|
114
|
+
"reports": "Informes",
|
|
114
115
|
"infrastructure": "Infraestructura",
|
|
115
116
|
"environmentSetup": "Configuración de entorno"
|
|
116
117
|
},
|
|
@@ -1461,6 +1462,70 @@
|
|
|
1461
1462
|
"title": "Activas ahora"
|
|
1462
1463
|
}
|
|
1463
1464
|
},
|
|
1465
|
+
"reports": {
|
|
1466
|
+
"title": "Informes",
|
|
1467
|
+
"refresh": "Actualizar",
|
|
1468
|
+
"loading": "Cargando informes…",
|
|
1469
|
+
"retry": "Reintentar",
|
|
1470
|
+
"error": "No se pudieron cargar los informes.",
|
|
1471
|
+
"period": "Del {from} al {to}",
|
|
1472
|
+
"unattributed": "Sin atribuir",
|
|
1473
|
+
"window": {
|
|
1474
|
+
"oneDay": "Últimas 24 horas",
|
|
1475
|
+
"sevenDays": "Últimos 7 días",
|
|
1476
|
+
"thirtyDays": "Últimos 30 días",
|
|
1477
|
+
"ninetyDays": "Últimos 90 días"
|
|
1478
|
+
},
|
|
1479
|
+
"filter": {
|
|
1480
|
+
"board": "Tablero",
|
|
1481
|
+
"allBoards": "Todos los tableros"
|
|
1482
|
+
},
|
|
1483
|
+
"dimension": {
|
|
1484
|
+
"workspace": "Tablero",
|
|
1485
|
+
"service": "Servicio",
|
|
1486
|
+
"taskType": "Tipo de tarea"
|
|
1487
|
+
},
|
|
1488
|
+
"breakdown": {
|
|
1489
|
+
"title": "Desglose por"
|
|
1490
|
+
},
|
|
1491
|
+
"totals": {
|
|
1492
|
+
"title": "En este periodo",
|
|
1493
|
+
"metered": "Gasto medido",
|
|
1494
|
+
"subscription": "Suscripción",
|
|
1495
|
+
"calls": "Llamadas al LLM",
|
|
1496
|
+
"tokens": "Tokens",
|
|
1497
|
+
"illustrative": "Los costes de suscripción son orientativos: esos planes son de tarifa plana, no se facturan por token y nunca cuentan como gasto."
|
|
1498
|
+
},
|
|
1499
|
+
"trend": {
|
|
1500
|
+
"title": "Gasto a lo largo del tiempo",
|
|
1501
|
+
"empty": "No se registró uso en este periodo."
|
|
1502
|
+
},
|
|
1503
|
+
"legend": {
|
|
1504
|
+
"metered": "Medido",
|
|
1505
|
+
"subscription": "Suscripción"
|
|
1506
|
+
},
|
|
1507
|
+
"spend": {
|
|
1508
|
+
"byModel": "Gasto por modelo",
|
|
1509
|
+
"byAgentKind": "Gasto por tipo de agente",
|
|
1510
|
+
"heading": "Gasto",
|
|
1511
|
+
"empty": "No se registró uso en este periodo.",
|
|
1512
|
+
"calls": "{count} llamada | {count} llamadas",
|
|
1513
|
+
"tokens": "{input} de entrada / {output} de salida",
|
|
1514
|
+
"subscriptionAside": "+{value} suscripción"
|
|
1515
|
+
},
|
|
1516
|
+
"activity": {
|
|
1517
|
+
"heading": "Ejecuciones",
|
|
1518
|
+
"empty": "No hubo ejecuciones en este periodo.",
|
|
1519
|
+
"runs": "{count} ejecución | {count} ejecuciones",
|
|
1520
|
+
"avg": "media {value}"
|
|
1521
|
+
},
|
|
1522
|
+
"status": {
|
|
1523
|
+
"done": "Completadas",
|
|
1524
|
+
"failed": "Fallidas",
|
|
1525
|
+
"running": "En curso",
|
|
1526
|
+
"other": "Otras"
|
|
1527
|
+
}
|
|
1528
|
+
},
|
|
1464
1529
|
"auth": {
|
|
1465
1530
|
"gate": {
|
|
1466
1531
|
"loading": "Cargando…"
|