+
+
+ {t('Log fallback mode')}
+
+ {t(
+ 'This legacy view estimates outcomes from eligible Consume and Error logs. Known billing adjustments and channel probes are excluded, but it is not a complete Relay Attempt trace. Logged latency uses only non-retry samples and remains accurate only to whole seconds.'
+ )}
+
+
+
+ {(retryBackfillPending || retryMetricsUnavailable) && (
+
+
+
+ {t('Retry and latency data are temporarily unavailable')}
+
+
+ {retryBackfillPending
+ ? t(
+ 'Historical retry markers are still being prepared. Request counts and estimated success rates remain available; retry counts and logged latency will appear after the backfill completes.'
+ )
+ : t(
+ 'The retry marker readiness state could not be verified. Request counts and estimated success rates remain available, but retry counts and logged latency are unavailable; check the application and log databases.'
+ )}
+
+
+ )}
+
+ {errorLogsDisabled && (
+
+
+ {t('Error log collection is disabled')}
+
+ {t(
+ 'The displayed error count includes only recorded logs and may be incomplete. Estimated success rate is unavailable while error log collection is disabled.'
+ )}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {performanceQuery.isPending && }
+ {showPerformanceError && (
+ performanceQuery.mutate(queryParams)}
+ />
+ )}
+ {showPerformanceIdle && (
+
+
+
+
+
+
+ {t('Channel performance is not loaded automatically')}
+
+
+ {t(
+ 'Select Apply filters or Refresh to query the current time range.'
+ )}
+
+
+
+ )}
+ {showPerformanceEmpty && (
+
+
+
+
+
+ {t('No recorded channel logs')}
+
+ {t(
+ 'Try a wider time range or check whether consumption and error logging are enabled.'
+ )}
+
+
+
+ )}
+ {showPerformanceTable && data && (
+
+
+ {data.items.length === 0 ? (
+
+
+
+
+
+ {t('No channel model performance')}
+
+ {t(
+ 'No eligible production logs were recorded for this channel in the last 24 hours.'
+ )}
+
+
+
+ ) : (
+
+ {t(
+ 'The detail table was truncated to {{count}} rows. The summary still covers all matching eligible logs.',
+ { count: data.items.length }
+ )}
+
+ )
+}
diff --git a/web/default/src/features/smart-ops/lib/channel-detail.test.ts b/web/default/src/features/smart-ops/lib/channel-detail.test.ts
new file mode 100644
index 00000000..4aa2242a
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/channel-detail.test.ts
@@ -0,0 +1,76 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+import type { ChannelPerformanceData, ChannelPerformanceItem } from '../types'
+import { getChannelDetailObservation } from './channel-detail'
+
+const selected: ChannelPerformanceItem = {
+ channel_id: 12,
+ channel_name: 'Test channel',
+ channel_type: 1,
+ channel_status: 1,
+ model_name: 'test-model',
+ effective_group: 'default',
+ observed_count: 10,
+ consume_log_count: 7,
+ error_log_count: 3,
+ consumed_quota: 750000,
+ retry_log_count: 0,
+ latency_sample_count: 7,
+ observed_success_rate: 70,
+ avg_logged_latency_ms: 1000,
+ last_observed_at: 3600,
+ probe_latency_ms: 100,
+ probe_test_time: 3500,
+ quality_flags: ['legacy'],
+}
+
+const detail: ChannelPerformanceData = {
+ storage_mode: 'legacy_log',
+ source: 'log_db',
+ partial: true,
+ quality_flags: ['legacy', 'partial'],
+ time_range: { start_at: 1, end_at: 3601, hours: 1 },
+ summary: {
+ channel_count: 1,
+ observed_count: 4,
+ consume_log_count: 3,
+ error_log_count: 1,
+ consumed_quota: 100,
+ retry_log_count: 0,
+ latency_sample_count: 3,
+ observed_success_rate: 75,
+ avg_logged_latency_ms: 1000,
+ last_observed_at: 7200,
+ },
+ items: [],
+ truncated: false,
+ generated_at: 7200,
+}
+
+describe('channel detail observation', () => {
+ test('uses channel-scoped detail evidence instead of the clicked list row', () => {
+ const observation = getChannelDetailObservation(selected, detail)
+
+ assert.equal(observation.lastObservedAt, detail.summary.last_observed_at)
+ assert.deepEqual(observation.qualityFlags, detail.quality_flags)
+ assert.equal(observation.probeLatencyMs, selected.probe_latency_ms)
+ })
+})
diff --git a/web/default/src/features/smart-ops/lib/channel-detail.ts b/web/default/src/features/smart-ops/lib/channel-detail.ts
new file mode 100644
index 00000000..330cfbb5
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/channel-detail.ts
@@ -0,0 +1,36 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import type { ChannelPerformanceData, ChannelPerformanceItem } from '../types'
+
+export type ChannelDetailObservation = {
+ lastObservedAt: number | null
+ qualityFlags: string[]
+ probeLatencyMs: number | null
+}
+
+export function getChannelDetailObservation(
+ selected: ChannelPerformanceItem,
+ detail: ChannelPerformanceData | undefined
+): ChannelDetailObservation {
+ return {
+ lastObservedAt: detail?.summary.last_observed_at ?? null,
+ qualityFlags: detail?.quality_flags ?? [],
+ probeLatencyMs: selected.probe_latency_ms,
+ }
+}
diff --git a/web/default/src/features/smart-ops/lib/filters.test.ts b/web/default/src/features/smart-ops/lib/filters.test.ts
new file mode 100644
index 00000000..1ddcdd4a
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/filters.test.ts
@@ -0,0 +1,87 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+import {
+ DEFAULT_FILTERS,
+ DEFAULT_MODEL_FILTERS,
+ DEFAULT_PERFORMANCE_HOURS,
+ MAX_PERFORMANCE_HOURS,
+ normalizeFilters,
+ normalizePerformanceHours,
+ toModelQuery,
+ toQuery,
+} from './filters'
+
+describe('production performance filters', () => {
+ test('defaults the initial view and invalid fallback to the latest hour', () => {
+ assert.equal(DEFAULT_FILTERS.hours, '1')
+ assert.equal(toQuery(DEFAULT_FILTERS).hours, DEFAULT_PERFORMANCE_HOURS)
+ assert.equal(
+ toQuery({ ...DEFAULT_FILTERS, hours: '' }).hours,
+ DEFAULT_PERFORMANCE_HOURS
+ )
+ })
+
+ test('accepts a custom integer hour range', () => {
+ assert.equal(toQuery({ ...DEFAULT_FILTERS, hours: '37' }).hours, 37)
+ assert.equal(
+ toModelQuery({ ...DEFAULT_MODEL_FILTERS, hours: '37' }).hours,
+ 37
+ )
+ })
+
+ test('normalizes custom hours to the supported query window', () => {
+ assert.equal(normalizePerformanceHours('0'), DEFAULT_PERFORMANCE_HOURS)
+ assert.equal(normalizePerformanceHours('-5'), DEFAULT_PERFORMANCE_HOURS)
+ assert.equal(normalizePerformanceHours('12.8'), 12)
+ assert.equal(normalizePerformanceHours('999'), MAX_PERFORMANCE_HOURS)
+ assert.equal(
+ normalizeFilters({ ...DEFAULT_FILTERS, hours: '999' }).hours,
+ String(MAX_PERFORMANCE_HOURS)
+ )
+ })
+
+ test('accepts only positive whole channel IDs', () => {
+ for (const channelId of ['', '0', '-3', '12abc', '12.5', '1e2']) {
+ assert.equal(
+ toQuery({ ...DEFAULT_FILTERS, channelId }).channelId,
+ undefined
+ )
+ }
+ assert.equal(toQuery({ ...DEFAULT_FILTERS, channelId: '12' }).channelId, 12)
+ })
+
+ test('trims optional text filters and drops whitespace-only values', () => {
+ assert.deepEqual(
+ toQuery({
+ ...DEFAULT_FILTERS,
+ model: ' alpha ',
+ group: ' prod ',
+ }),
+ {
+ hours: DEFAULT_PERFORMANCE_HOURS,
+ channelId: undefined,
+ model: 'alpha',
+ group: 'prod',
+ }
+ )
+ assert.equal(toQuery({ ...DEFAULT_FILTERS, model: ' ' }).model, undefined)
+ })
+})
diff --git a/web/default/src/features/smart-ops/lib/filters.ts b/web/default/src/features/smart-ops/lib/filters.ts
new file mode 100644
index 00000000..3483b4a8
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/filters.ts
@@ -0,0 +1,91 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import type { ChannelPerformanceQuery, ModelPerformanceQuery } from '../types'
+
+export type FilterDraft = {
+ hours: string
+ channelId: string
+ model: string
+ group: string
+}
+
+export const DEFAULT_PERFORMANCE_HOURS = 1
+export const MAX_PERFORMANCE_HOURS = 168
+export const MAX_PERFORMANCE_LIMIT = 200
+
+export const DEFAULT_FILTERS: FilterDraft = {
+ hours: String(DEFAULT_PERFORMANCE_HOURS),
+ channelId: '',
+ model: '',
+ group: '',
+}
+
+export function normalizePerformanceHours(value: string): number {
+ const parsed = Number(value)
+ if (!Number.isFinite(parsed)) return DEFAULT_PERFORMANCE_HOURS
+ return Math.min(
+ Math.max(Math.trunc(parsed), DEFAULT_PERFORMANCE_HOURS),
+ MAX_PERFORMANCE_HOURS
+ )
+}
+
+export function normalizeFilters(filters: T): T {
+ return {
+ ...filters,
+ hours: String(normalizePerformanceHours(filters.hours)),
+ } as T
+}
+
+export function toQuery(filters: FilterDraft): ChannelPerformanceQuery {
+ const channelId = parsePositiveChannelId(filters.channelId)
+ return {
+ hours: normalizePerformanceHours(filters.hours),
+ channelId,
+ model: filters.model.trim() || undefined,
+ group: filters.group.trim() || undefined,
+ }
+}
+
+function parsePositiveChannelId(value: string): number | undefined {
+ const normalized = value.trim()
+ if (!/^\+?\d+$/.test(normalized)) return undefined
+ const channelId = Number(normalized)
+ if (!Number.isSafeInteger(channelId) || channelId <= 0) return undefined
+ return channelId
+}
+
+export type ModelFilterDraft = {
+ hours: string
+ model: string
+ group: string
+}
+
+export const DEFAULT_MODEL_FILTERS: ModelFilterDraft = {
+ hours: String(DEFAULT_PERFORMANCE_HOURS),
+ model: '',
+ group: '',
+}
+
+export function toModelQuery(filters: ModelFilterDraft): ModelPerformanceQuery {
+ return {
+ hours: normalizePerformanceHours(filters.hours),
+ model: filters.model.trim() || undefined,
+ group: filters.group.trim() || undefined,
+ }
+}
diff --git a/web/default/src/features/smart-ops/lib/format.test.ts b/web/default/src/features/smart-ops/lib/format.test.ts
new file mode 100644
index 00000000..0fd45386
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/format.test.ts
@@ -0,0 +1,33 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+import { formatLegacyLatency } from './format'
+
+describe('formatLegacyLatency', () => {
+ test('keeps a recorded zero-second average distinct from missing data', () => {
+ assert.equal(formatLegacyLatency(0), '0ms')
+ assert.equal(formatLegacyLatency(null), '—')
+ })
+
+ test('uses the shared latency format for positive values', () => {
+ assert.equal(formatLegacyLatency(500), '500ms')
+ assert.equal(formatLegacyLatency(1_500), '1.50s')
+ })
+})
diff --git a/web/default/src/features/smart-ops/lib/format.ts b/web/default/src/features/smart-ops/lib/format.ts
new file mode 100644
index 00000000..d4284678
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/format.ts
@@ -0,0 +1,33 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { formatLatency } from '@/features/performance-metrics/lib/format'
+
+export function formatLegacyLatency(
+ milliseconds: number | null | undefined
+): string {
+ if (
+ milliseconds == null ||
+ !Number.isFinite(milliseconds) ||
+ milliseconds < 0
+ ) {
+ return '—'
+ }
+ if (milliseconds === 0) return '0ms'
+ return formatLatency(milliseconds)
+}
diff --git a/web/default/src/features/smart-ops/lib/model-sort.test.ts b/web/default/src/features/smart-ops/lib/model-sort.test.ts
new file mode 100644
index 00000000..88bc7283
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/model-sort.test.ts
@@ -0,0 +1,142 @@
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+import type { ModelPerformanceItem } from '../types'
+import {
+ sortModelPerformanceItems,
+ type ModelPerformanceSortKey,
+} from './model-sort'
+
+function performanceItem(
+ modelName: string,
+ values: Partial
+): ModelPerformanceItem {
+ return {
+ model_name: modelName,
+ channel_count: 1,
+ observed_count: 0,
+ consume_log_count: 0,
+ error_log_count: 0,
+ consumed_quota: 0,
+ retry_log_count: 0,
+ latency_sample_count: 0,
+ observed_success_rate: null,
+ avg_logged_latency_ms: null,
+ avg_tps: null,
+ last_observed_at: 1,
+ quality_flags: [],
+ ...values,
+ }
+}
+
+const rows = [
+ performanceItem('Balanced model', {
+ channel_count: 2,
+ observed_count: 20,
+ error_log_count: 2,
+ consumed_quota: 800,
+ observed_success_rate: 90,
+ avg_logged_latency_ms: 1000,
+ avg_tps: 60,
+ retry_log_count: 1,
+ }),
+ performanceItem('Problem model', {
+ channel_count: 1,
+ observed_count: 5,
+ error_log_count: 8,
+ consumed_quota: 200,
+ observed_success_rate: 40,
+ avg_logged_latency_ms: 4500,
+ avg_tps: 10,
+ retry_log_count: 4,
+ }),
+ performanceItem('Unknown metrics', {
+ observed_count: 10,
+ error_log_count: 1,
+ consumed_quota: 0,
+ retry_log_count: null,
+ avg_tps: null,
+ }),
+]
+
+describe('sortModelPerformanceItems', () => {
+ const cases: Array<{
+ key: ModelPerformanceSortKey
+ asc: string[]
+ desc: string[]
+ }> = [
+ {
+ key: 'model_name',
+ asc: ['Balanced model', 'Problem model', 'Unknown metrics'],
+ desc: ['Unknown metrics', 'Problem model', 'Balanced model'],
+ },
+ {
+ key: 'channel_count',
+ asc: ['Problem model', 'Unknown metrics', 'Balanced model'],
+ desc: ['Balanced model', 'Problem model', 'Unknown metrics'],
+ },
+ {
+ key: 'observed_count',
+ asc: ['Problem model', 'Unknown metrics', 'Balanced model'],
+ desc: ['Balanced model', 'Unknown metrics', 'Problem model'],
+ },
+ {
+ key: 'error_log_count',
+ asc: ['Unknown metrics', 'Balanced model', 'Problem model'],
+ desc: ['Problem model', 'Balanced model', 'Unknown metrics'],
+ },
+ {
+ key: 'consumed_quota',
+ asc: ['Unknown metrics', 'Problem model', 'Balanced model'],
+ desc: ['Balanced model', 'Problem model', 'Unknown metrics'],
+ },
+ {
+ key: 'observed_success_rate',
+ asc: ['Problem model', 'Balanced model', 'Unknown metrics'],
+ desc: ['Balanced model', 'Problem model', 'Unknown metrics'],
+ },
+ {
+ key: 'avg_logged_latency_ms',
+ asc: ['Balanced model', 'Problem model', 'Unknown metrics'],
+ desc: ['Problem model', 'Balanced model', 'Unknown metrics'],
+ },
+ {
+ key: 'avg_tps',
+ asc: ['Problem model', 'Balanced model', 'Unknown metrics'],
+ desc: ['Balanced model', 'Problem model', 'Unknown metrics'],
+ },
+ {
+ key: 'retry_log_count',
+ asc: ['Balanced model', 'Problem model', 'Unknown metrics'],
+ desc: ['Problem model', 'Balanced model', 'Unknown metrics'],
+ },
+ ]
+
+ for (const { key, asc, desc } of cases) {
+ test(`sorts ${key} in both directions and keeps missing values last`, () => {
+ assert.deepEqual(
+ sortModelPerformanceItems(rows, { key, direction: 'asc' }).map(
+ (item) => item.model_name
+ ),
+ asc
+ )
+ assert.deepEqual(
+ sortModelPerformanceItems(rows, { key, direction: 'desc' }).map(
+ (item) => item.model_name
+ ),
+ desc
+ )
+ })
+ }
+
+ test('does not mutate the API result order', () => {
+ const originalOrder = rows.map((item) => item.model_name)
+ sortModelPerformanceItems(rows, {
+ key: 'error_log_count',
+ direction: 'desc',
+ })
+ assert.deepEqual(
+ rows.map((item) => item.model_name),
+ originalOrder
+ )
+ })
+})
diff --git a/web/default/src/features/smart-ops/lib/model-sort.ts b/web/default/src/features/smart-ops/lib/model-sort.ts
new file mode 100644
index 00000000..5ec956aa
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/model-sort.ts
@@ -0,0 +1,55 @@
+import type { ModelPerformanceItem } from '../types'
+
+export type ModelPerformanceSortKey =
+ | 'model_name'
+ | 'channel_count'
+ | 'observed_count'
+ | 'error_log_count'
+ | 'consumed_quota'
+ | 'observed_success_rate'
+ | 'avg_logged_latency_ms'
+ | 'avg_tps'
+ | 'retry_log_count'
+
+export type ModelPerformanceSortState = {
+ key: ModelPerformanceSortKey
+ direction: 'asc' | 'desc'
+} | null
+
+function sortableValue(
+ item: ModelPerformanceItem,
+ key: ModelPerformanceSortKey
+): number | string | null {
+ const value = item[key]
+ if (key === 'model_name') return typeof value === 'string' ? value : null
+ return typeof value === 'number' && Number.isFinite(value) ? value : null
+}
+
+export function sortModelPerformanceItems(
+ items: readonly ModelPerformanceItem[],
+ sortState: ModelPerformanceSortState,
+ locale?: string
+): readonly ModelPerformanceItem[] {
+ if (!sortState) return items
+
+ return items
+ .map((item, originalIndex) => ({ item, originalIndex }))
+ .sort((left, right) => {
+ const leftValue = sortableValue(left.item, sortState.key)
+ const rightValue = sortableValue(right.item, sortState.key)
+
+ if (leftValue == null && rightValue == null) {
+ return left.originalIndex - right.originalIndex
+ }
+ if (leftValue == null) return 1
+ if (rightValue == null) return -1
+
+ const comparison =
+ typeof leftValue === 'string' && typeof rightValue === 'string'
+ ? leftValue.localeCompare(rightValue, locale)
+ : Number(leftValue) - Number(rightValue)
+ if (comparison === 0) return left.originalIndex - right.originalIndex
+ return sortState.direction === 'asc' ? comparison : -comparison
+ })
+ .map(({ item }) => item)
+}
diff --git a/web/default/src/features/smart-ops/lib/sort-direction-icon.tsx b/web/default/src/features/smart-ops/lib/sort-direction-icon.tsx
new file mode 100644
index 00000000..3c11be2e
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/sort-direction-icon.tsx
@@ -0,0 +1,36 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'
+import type { SortDirection } from './sort-direction'
+
+type SortDirectionIconProps = {
+ direction: SortDirection
+}
+
+export function SortDirectionIcon(
+ props: SortDirectionIconProps
+): React.ReactElement {
+ if (props.direction === 'desc') {
+ return
+ }
+ if (props.direction === 'asc') {
+ return
+ }
+ return
+}
diff --git a/web/default/src/features/smart-ops/lib/sort-direction.ts b/web/default/src/features/smart-ops/lib/sort-direction.ts
new file mode 100644
index 00000000..3cfc2dde
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/sort-direction.ts
@@ -0,0 +1,27 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+export type SortDirection = 'asc' | 'desc' | null
+
+export function getAriaSort(
+ direction: SortDirection
+): 'ascending' | 'descending' | 'none' {
+ if (direction === 'asc') return 'ascending'
+ if (direction === 'desc') return 'descending'
+ return 'none'
+}
diff --git a/web/default/src/features/smart-ops/lib/sort.test.ts b/web/default/src/features/smart-ops/lib/sort.test.ts
new file mode 100644
index 00000000..c17c5e0d
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/sort.test.ts
@@ -0,0 +1,155 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+import type { ChannelPerformanceItem } from '../types'
+import {
+ sortChannelPerformanceItems,
+ type ChannelPerformanceSortKey,
+} from './sort'
+
+function performanceItem(
+ channelName: string,
+ values: Partial
+): ChannelPerformanceItem {
+ return {
+ channel_id: values.channel_id ?? 1,
+ channel_name: channelName,
+ channel_type: 1,
+ channel_status: 1,
+ model_name: 'test-model',
+ effective_group: 'default',
+ observed_count: 0,
+ consume_log_count: 0,
+ error_log_count: 0,
+ consumed_quota: 0,
+ retry_log_count: 0,
+ latency_sample_count: 0,
+ observed_success_rate: null,
+ avg_logged_latency_ms: null,
+ last_observed_at: 1,
+ probe_latency_ms: null,
+ probe_test_time: null,
+ quality_flags: [],
+ ...values,
+ }
+}
+
+const rows = [
+ performanceItem('Balanced', {
+ channel_id: 1,
+ observed_count: 20,
+ error_log_count: 2,
+ consumed_quota: 800,
+ observed_success_rate: 90,
+ avg_logged_latency_ms: 1000,
+ retry_log_count: 1,
+ probe_latency_ms: 80,
+ }),
+ performanceItem('Problematic', {
+ channel_id: 2,
+ observed_count: 5,
+ error_log_count: 8,
+ consumed_quota: 200,
+ observed_success_rate: 40,
+ avg_logged_latency_ms: 4500,
+ retry_log_count: 4,
+ probe_latency_ms: 900,
+ }),
+ performanceItem('Unknown metrics', {
+ channel_id: 3,
+ observed_count: 10,
+ error_log_count: 1,
+ consumed_quota: 0,
+ retry_log_count: null,
+ }),
+]
+
+describe('sortChannelPerformanceItems', () => {
+ const cases: Array<{
+ key: ChannelPerformanceSortKey
+ asc: string[]
+ desc: string[]
+ }> = [
+ {
+ key: 'observed_count',
+ asc: ['Problematic', 'Unknown metrics', 'Balanced'],
+ desc: ['Balanced', 'Unknown metrics', 'Problematic'],
+ },
+ {
+ key: 'error_log_count',
+ asc: ['Unknown metrics', 'Balanced', 'Problematic'],
+ desc: ['Problematic', 'Balanced', 'Unknown metrics'],
+ },
+ {
+ key: 'consumed_quota',
+ asc: ['Unknown metrics', 'Problematic', 'Balanced'],
+ desc: ['Balanced', 'Problematic', 'Unknown metrics'],
+ },
+ {
+ key: 'observed_success_rate',
+ asc: ['Problematic', 'Balanced', 'Unknown metrics'],
+ desc: ['Balanced', 'Problematic', 'Unknown metrics'],
+ },
+ {
+ key: 'avg_logged_latency_ms',
+ asc: ['Balanced', 'Problematic', 'Unknown metrics'],
+ desc: ['Problematic', 'Balanced', 'Unknown metrics'],
+ },
+ {
+ key: 'retry_log_count',
+ asc: ['Balanced', 'Problematic', 'Unknown metrics'],
+ desc: ['Problematic', 'Balanced', 'Unknown metrics'],
+ },
+ {
+ key: 'probe_latency_ms',
+ asc: ['Balanced', 'Problematic', 'Unknown metrics'],
+ desc: ['Problematic', 'Balanced', 'Unknown metrics'],
+ },
+ ]
+
+ for (const { key, asc, desc } of cases) {
+ test(`sorts ${key} in both directions and keeps missing values last`, () => {
+ assert.deepEqual(
+ sortChannelPerformanceItems(rows, { key, direction: 'asc' }).map(
+ (item) => item.channel_name
+ ),
+ asc
+ )
+ assert.deepEqual(
+ sortChannelPerformanceItems(rows, { key, direction: 'desc' }).map(
+ (item) => item.channel_name
+ ),
+ desc
+ )
+ })
+ }
+
+ test('does not mutate the API result order', () => {
+ const originalOrder = rows.map((item) => item.channel_name)
+ sortChannelPerformanceItems(rows, {
+ key: 'error_log_count',
+ direction: 'desc',
+ })
+ assert.deepEqual(
+ rows.map((item) => item.channel_name),
+ originalOrder
+ )
+ })
+})
diff --git a/web/default/src/features/smart-ops/lib/sort.ts b/web/default/src/features/smart-ops/lib/sort.ts
new file mode 100644
index 00000000..54c122b2
--- /dev/null
+++ b/web/default/src/features/smart-ops/lib/sort.ts
@@ -0,0 +1,66 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import type { ChannelPerformanceItem } from '../types'
+
+export type ChannelPerformanceSortKey =
+ | 'observed_count'
+ | 'error_log_count'
+ | 'consumed_quota'
+ | 'observed_success_rate'
+ | 'avg_logged_latency_ms'
+ | 'retry_log_count'
+ | 'probe_latency_ms'
+
+export type ChannelPerformanceSortState = {
+ key: ChannelPerformanceSortKey
+ direction: 'asc' | 'desc'
+} | null
+
+function sortableMetric(
+ item: ChannelPerformanceItem,
+ key: ChannelPerformanceSortKey
+): number | null {
+ const value = item[key]
+ return typeof value === 'number' && Number.isFinite(value) ? value : null
+}
+
+export function sortChannelPerformanceItems(
+ items: readonly ChannelPerformanceItem[],
+ sortState: ChannelPerformanceSortState
+): readonly ChannelPerformanceItem[] {
+ if (!sortState) return items
+
+ return items
+ .map((item, originalIndex) => ({ item, originalIndex }))
+ .sort((left, right) => {
+ const leftValue = sortableMetric(left.item, sortState.key)
+ const rightValue = sortableMetric(right.item, sortState.key)
+
+ if (leftValue == null && rightValue == null) {
+ return left.originalIndex - right.originalIndex
+ }
+ if (leftValue == null) return 1
+ if (rightValue == null) return -1
+
+ const comparison = leftValue - rightValue
+ if (comparison === 0) return left.originalIndex - right.originalIndex
+ return sortState.direction === 'asc' ? comparison : -comparison
+ })
+ .map(({ item }) => item)
+}
diff --git a/web/default/src/features/smart-ops/model-performance.test.tsx b/web/default/src/features/smart-ops/model-performance.test.tsx
new file mode 100644
index 00000000..b793f2e4
--- /dev/null
+++ b/web/default/src/features/smart-ops/model-performance.test.tsx
@@ -0,0 +1,744 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { act } from 'react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { createReactTestEnvironment } from '@/test/react'
+import { waitFor } from '@testing-library/react'
+import assert from 'node:assert/strict'
+import { after, before, describe, test } from 'node:test'
+import { api } from '@/lib/api'
+import { formatQuota } from '@/lib/format'
+import { formatThroughput } from '@/features/performance-metrics/lib/format'
+import type {
+ PerformanceAggregate,
+ PerformanceGroup,
+} from '@/features/performance-metrics/types'
+import {
+ buildLatencyChartData,
+ buildUptimeChartData,
+ downsampleUptimeSeries,
+ getUptimeAxisDomain,
+} from '@/features/pricing/components/model-details-chart-utils'
+import { ModelPerformanceDetailsContent } from '@/features/pricing/components/model-details-performance'
+import { UptimeSparkline } from '@/features/pricing/components/model-details-uptime-sparkline'
+import type { ModelPerformanceData } from './types'
+
+const testEnv = createReactTestEnvironment()
+let ModelPerformance: typeof import('./model-performance').ModelPerformance
+let ModelPerformanceCollectionStateAlert: typeof import('./model-performance').ModelPerformanceCollectionStateAlert
+let ModelPerformanceDetailGrid: typeof import('./model-performance').ModelPerformanceDetailGrid
+
+before(async () => {
+ await testEnv.setup()
+ const module = await import('./model-performance')
+ ModelPerformance = module.ModelPerformance
+ ModelPerformanceCollectionStateAlert =
+ module.ModelPerformanceCollectionStateAlert
+ ModelPerformanceDetailGrid = module.ModelPerformanceDetailGrid
+})
+
+after(() => testEnv.teardown())
+
+const modelPerformanceData: ModelPerformanceData = {
+ storage_mode: 'legacy_log',
+ source: 'log_db',
+ partial: true,
+ quality_flags: ['legacy'],
+ time_range: { start_at: 1, end_at: 3601, hours: 1 },
+ throughput: {
+ collection_state: 'available',
+ coverage: {
+ requested_start_at: 1,
+ requested_end_at: 3601,
+ bucket_start_at: 3600,
+ bucket_end_at: 3601,
+ bucket_seconds: 3600,
+ granularity_state: 'unknown',
+ approximate: true,
+ },
+ },
+ summary: {
+ model_count: 2,
+ channel_count: 3,
+ observed_count: 15,
+ consume_log_count: 12,
+ error_log_count: 3,
+ consumed_quota: 1200000,
+ retry_log_count: 2,
+ latency_sample_count: 12,
+ observed_success_rate: 80,
+ avg_logged_latency_ms: 1200,
+ last_observed_at: 3600,
+ },
+ items: [
+ {
+ model_name: 'alpha',
+ channel_count: 2,
+ observed_count: 10,
+ consume_log_count: 8,
+ error_log_count: 2,
+ consumed_quota: 800000,
+ retry_log_count: 1,
+ latency_sample_count: 8,
+ observed_success_rate: 80,
+ avg_logged_latency_ms: 1000,
+ avg_tps: 48.5,
+ last_observed_at: 3600,
+ quality_flags: ['legacy'],
+ },
+ {
+ model_name: 'beta',
+ channel_count: 1,
+ observed_count: 5,
+ consume_log_count: 4,
+ error_log_count: 1,
+ consumed_quota: 400000,
+ retry_log_count: 1,
+ latency_sample_count: 4,
+ observed_success_rate: 80,
+ avg_logged_latency_ms: 1600,
+ avg_tps: 12.25,
+ last_observed_at: 3500,
+ quality_flags: ['legacy'],
+ },
+ ],
+ truncated: false,
+ generated_at: 3601,
+}
+
+function renderModelPerformance() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ })
+ return {
+ queryClient,
+ render: testEnv.render(
+
+
+
+ ),
+ }
+}
+
+describe('ModelPerformance manual queries', () => {
+ test('does not query on mount and sends the default hour range only after Apply filters', async () => {
+ const originalGet = api.get
+ let requestCount = 0
+ let requestParams: Record | undefined
+ api.get = (async (_url, config) => {
+ requestCount += 1
+ requestParams = config?.params as Record
+ return { data: { success: true, data: modelPerformanceData } }
+ }) as typeof api.get
+
+ const { queryClient, render } = renderModelPerformance()
+ const view = await render
+
+ try {
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0))
+ })
+ assert.equal(requestCount, 0, 'mounting must not query model logs')
+
+ const applyButton = Array.from(
+ view.container.querySelectorAll('button')
+ ).find((candidate) => candidate.textContent?.trim() === 'Apply filters')
+ assert.ok(applyButton, 'expected Apply filters button')
+ await view.click(applyButton)
+
+ await waitFor(() => assert.equal(requestCount, 1))
+ assert.equal(requestParams?.hours, 1)
+ assert.equal(requestParams?.limit, 200)
+ assert.equal(requestParams?.model, undefined)
+ assert.equal(requestParams?.group, undefined)
+ } finally {
+ api.get = originalGet
+ queryClient.clear()
+ await view.unmount()
+ }
+ })
+
+ test('renders every returned model with errors, consumed quota, and throughput', async () => {
+ const originalGet = api.get
+ api.get = (async () => ({
+ data: { success: true, data: modelPerformanceData },
+ })) as typeof api.get
+
+ const { queryClient, render } = renderModelPerformance()
+ const view = await render
+
+ try {
+ const applyButton = Array.from(
+ view.container.querySelectorAll('button')
+ ).find((candidate) => candidate.textContent?.trim() === 'Apply filters')
+ assert.ok(applyButton, 'expected Apply filters button')
+ await view.click(applyButton)
+
+ await waitFor(() => {
+ assert.equal(view.container.querySelectorAll('tbody tr').length, 2)
+ })
+
+ assert.ok(view.container.textContent?.includes('alpha'))
+ assert.ok(view.container.textContent?.includes('beta'))
+ const errorCardLabel = Array.from(
+ view.container.querySelectorAll('[data-slot="card-description"]')
+ ).find((node) => node.textContent?.trim() === 'Recorded error logs')
+ assert.ok(errorCardLabel, 'expected recorded error summary card')
+ assert.equal(
+ errorCardLabel.parentElement?.querySelector('[data-slot="card-title"]')
+ ?.textContent,
+ '3'
+ )
+ const headers = Array.from(view.container.querySelectorAll('thead th'))
+ const errorColumnIndex = headers.findIndex(
+ (header) => header.textContent?.trim() === 'Recorded error logs'
+ )
+ assert.ok(errorColumnIndex >= 0, 'expected error count column')
+ const rows = view.container.querySelectorAll('tbody tr')
+ assert.equal(
+ rows
+ .item(0)
+ .querySelectorAll('td')
+ .item(errorColumnIndex)
+ .textContent?.trim(),
+ '2'
+ )
+ assert.equal(
+ rows
+ .item(1)
+ .querySelectorAll('td')
+ .item(errorColumnIndex)
+ .textContent?.trim(),
+ '1'
+ )
+ const consumedQuotaColumnIndex = headers.findIndex(
+ (header) => header.textContent?.trim() === 'Consumed quota'
+ )
+ assert.ok(consumedQuotaColumnIndex >= 0, 'expected consumed quota column')
+ assert.equal(
+ rows
+ .item(0)
+ .querySelectorAll('td')
+ .item(consumedQuotaColumnIndex)
+ .textContent?.trim(),
+ formatQuota(800000)
+ )
+ const throughputColumnIndex = headers.findIndex(
+ (header) => header.textContent?.trim() === 'Throughput'
+ )
+ assert.ok(throughputColumnIndex >= 0, 'expected throughput column')
+ assert.equal(
+ rows
+ .item(0)
+ .querySelectorAll('td')
+ .item(throughputColumnIndex)
+ .textContent?.trim(),
+ formatThroughput(48.5)
+ )
+ } finally {
+ api.get = originalGet
+ queryClient.clear()
+ await view.unmount()
+ }
+ })
+
+ test('exposes a sort menu for each model performance metric', async () => {
+ const originalGet = api.get
+ api.get = (async () => ({
+ data: { success: true, data: modelPerformanceData },
+ })) as typeof api.get
+
+ const { queryClient, render } = renderModelPerformance()
+ const view = await render
+
+ try {
+ const applyButton = Array.from(
+ view.container.querySelectorAll('button')
+ ).find((candidate) => candidate.textContent?.trim() === 'Apply filters')
+ assert.ok(applyButton, 'expected Apply filters button')
+ await view.click(applyButton)
+
+ const sortableLabels = [
+ 'Model',
+ 'Channels',
+ 'Observed model log events',
+ 'Recorded error logs',
+ 'Consumed quota',
+ 'Estimated success rate',
+ 'Logged latency',
+ 'Throughput',
+ 'Retries',
+ ]
+ await waitFor(() => {
+ const buttons = Array.from(
+ view.container.querySelectorAll('thead button')
+ )
+ for (const label of sortableLabels) {
+ assert.ok(
+ buttons.some((button) => button.textContent?.trim() === label),
+ `expected sortable ${label} header`
+ )
+ }
+ })
+ } finally {
+ api.get = originalGet
+ queryClient.clear()
+ await view.unmount()
+ }
+ })
+
+ test('distinguishes disabled, empty, and failed throughput collection states', async () => {
+ const originalGet = api.get
+ const scenarios = [
+ {
+ state: 'collection_disabled' as const,
+ flag: 'throughput_collection_disabled',
+ title: 'Performance metrics collection is disabled',
+ },
+ {
+ state: 'no_samples' as const,
+ flag: 'throughput_no_samples',
+ title: 'No throughput samples',
+ },
+ {
+ state: 'query_failed' as const,
+ flag: 'throughput_query_failed',
+ title: 'Throughput query failed',
+ },
+ {
+ state: 'partial' as const,
+ flag: 'throughput_partial',
+ title: 'Throughput data is partial',
+ },
+ ]
+
+ try {
+ for (const scenario of scenarios) {
+ api.get = (async () => ({
+ data: {
+ success: true,
+ data: {
+ ...modelPerformanceData,
+ quality_flags: [scenario.flag],
+ throughput: {
+ ...modelPerformanceData.throughput,
+ collection_state: scenario.state,
+ },
+ },
+ },
+ })) as typeof api.get
+
+ const { queryClient, render } = renderModelPerformance()
+ const view = await render
+ try {
+ const applyButton = Array.from(
+ view.container.querySelectorAll('button')
+ ).find(
+ (candidate) => candidate.textContent?.trim() === 'Apply filters'
+ )
+ assert.ok(applyButton, 'expected Apply filters button')
+ await view.click(applyButton)
+ await waitFor(() => {
+ assert.ok(view.container.textContent?.includes(scenario.title))
+ })
+ } finally {
+ queryClient.clear()
+ await view.unmount()
+ }
+ }
+ } finally {
+ api.get = originalGet
+ }
+ })
+
+ test('does not claim a precise bucket size when historical granularity is unknown', async () => {
+ const originalGet = api.get
+ api.get = (async () => ({
+ data: {
+ success: true,
+ data: {
+ ...modelPerformanceData,
+ quality_flags: ['throughput_window_approximate'],
+ },
+ },
+ })) as typeof api.get
+
+ const { queryClient, render } = renderModelPerformance()
+ const view = await render
+
+ try {
+ const applyButton = Array.from(
+ view.container.querySelectorAll('button')
+ ).find((candidate) => candidate.textContent?.trim() === 'Apply filters')
+ assert.ok(applyButton, 'expected Apply filters button')
+ await view.click(applyButton)
+
+ await waitFor(() => {
+ const text = view.container.textContent ?? ''
+ assert.ok(text.includes('Bucket granularity is unknown or mixed'))
+ assert.ok(!text.includes('60 minute buckets'))
+ })
+ } finally {
+ api.get = originalGet
+ queryClient.clear()
+ await view.unmount()
+ }
+ })
+
+ test('loads 24-hour model performance only after opening details', async () => {
+ const originalGet = api.get
+ let detailRequests = 0
+ api.get = (async (url, config) => {
+ if (url === '/api/smart-ops/model-performance/detail') {
+ detailRequests += 1
+ assert.equal(config?.params?.model, 'alpha')
+ return {
+ data: {
+ success: true,
+ data: {
+ model_name: 'alpha',
+ series_schema: 'test',
+ collection_state: 'query_failed',
+ coverage: {
+ requested_start_at: 1,
+ requested_end_at: 3601,
+ bucket_start_at: 3600,
+ bucket_end_at: 3601,
+ bucket_seconds: 3600,
+ granularity_state: 'unknown',
+ approximate: true,
+ },
+ summary: {
+ avg_ttft_ms: 320,
+ avg_latency_ms: 1200,
+ success_rate: 99.5,
+ avg_tps: 48.5,
+ series: [
+ {
+ ts: 3600,
+ avg_ttft_ms: 320,
+ avg_latency_ms: 1200,
+ success_rate: 99.5,
+ avg_tps: 48.5,
+ },
+ ],
+ },
+ groups: [
+ {
+ group: 'default',
+ avg_ttft_ms: 320,
+ avg_latency_ms: 1200,
+ success_rate: 99.5,
+ avg_tps: 48.5,
+ series: [
+ {
+ ts: 3600,
+ avg_ttft_ms: 320,
+ avg_latency_ms: 1200,
+ success_rate: 99.5,
+ avg_tps: 48.5,
+ },
+ ],
+ },
+ ],
+ },
+ },
+ }
+ }
+ return { data: { success: true, data: modelPerformanceData } }
+ }) as typeof api.get
+
+ const { queryClient, render } = renderModelPerformance()
+ const view = await render
+
+ try {
+ const applyButton = Array.from(
+ view.container.querySelectorAll('button')
+ ).find((candidate) => candidate.textContent?.trim() === 'Apply filters')
+ assert.ok(applyButton, 'expected Apply filters button')
+ await view.click(applyButton)
+ await waitFor(() => {
+ assert.equal(view.container.querySelectorAll('tbody tr').length, 2)
+ })
+ assert.equal(detailRequests, 0, 'list loading must not fetch detail data')
+
+ const detailsButton = Array.from(
+ view.container.querySelectorAll('tbody button')
+ ).find((candidate) => candidate.textContent?.trim() === 'Details')
+ assert.ok(detailsButton, 'expected model details button')
+ await view.click(detailsButton)
+
+ await waitFor(() => {
+ assert.equal(detailRequests, 1)
+ })
+ } finally {
+ api.get = originalGet
+ queryClient.clear()
+ await view.unmount()
+ }
+ })
+
+ test('renders an explicit query-failed notice for model detail throughput', async () => {
+ const view = await testEnv.render(
+
+ )
+
+ try {
+ const text = view.container.textContent ?? ''
+ assert.ok(text.includes('Throughput query failed'))
+ assert.ok(text.includes('check the application database'))
+ } finally {
+ await view.unmount()
+ }
+ })
+
+ test('renders per-group performance and 24-hour trend sections', async () => {
+ const groups: PerformanceGroup[] = [
+ {
+ group: 'default',
+ avg_ttft_ms: 320,
+ avg_latency_ms: 1200,
+ success_rate: 99.5,
+ avg_tps: 48.5,
+ series: [
+ {
+ ts: 3600,
+ avg_ttft_ms: 320,
+ avg_latency_ms: 1200,
+ success_rate: 99.5,
+ avg_tps: 48.5,
+ },
+ ],
+ },
+ ]
+ const summary: PerformanceAggregate = {
+ avg_ttft_ms: 320,
+ avg_latency_ms: 1200,
+ success_rate: 99.5,
+ avg_tps: 48.5,
+ series: groups[0].series,
+ }
+ const view = await testEnv.render(
+
+ )
+
+ try {
+ const detailText = view.container.textContent ?? ''
+ assert.ok(detailText.includes('Per-group performance'))
+ assert.ok(detailText.includes('Latency trend (last 24h)'))
+ assert.ok(detailText.includes('Availability (last 24h)'))
+ assert.ok(detailText.includes('default'))
+ assert.ok(detailText.includes(formatThroughput(48.5)))
+ } finally {
+ await view.unmount()
+ }
+ })
+
+ test('prevents the model detail summary grid from collapsing in the sheet', async () => {
+ const view = await testEnv.render(
+
+
+
+ )
+
+ try {
+ const detailGrid = view.container.querySelector('dl')
+ assert.ok(detailGrid, 'expected model detail grid')
+ assert.ok(
+ detailGrid.classList.contains('shrink-0'),
+ 'model detail grid must not collapse inside a constrained flex sheet'
+ )
+ } finally {
+ await view.unmount()
+ }
+ })
+
+ test('uses the backend weighted model aggregate instead of averaging groups', async () => {
+ const groups: PerformanceGroup[] = [
+ {
+ group: 'small',
+ avg_ttft_ms: 1000,
+ avg_latency_ms: 1000,
+ success_rate: 0,
+ avg_tps: 10,
+ series: [
+ {
+ ts: 3600,
+ avg_ttft_ms: 1000,
+ avg_latency_ms: 1000,
+ success_rate: 0,
+ avg_tps: 10,
+ },
+ ],
+ },
+ {
+ group: 'large',
+ avg_ttft_ms: 50,
+ avg_latency_ms: 100,
+ success_rate: 100,
+ avg_tps: 100,
+ series: [
+ {
+ ts: 3600,
+ avg_ttft_ms: 50,
+ avg_latency_ms: 100,
+ success_rate: 100,
+ avg_tps: 100,
+ },
+ ],
+ },
+ ]
+ const summary: PerformanceAggregate = {
+ avg_ttft_ms: 50,
+ avg_latency_ms: 109,
+ success_rate: 99,
+ avg_tps: 91.743,
+ series: [],
+ }
+ const view = await testEnv.render(
+
+ )
+
+ try {
+ const detailText = view.container.textContent ?? ''
+ assert.ok(detailText.includes('99.00%'))
+ assert.ok(detailText.includes(formatThroughput(91.743)))
+ assert.ok(!detailText.includes('50.00%'))
+ } finally {
+ await view.unmount()
+ }
+ })
+
+ test('keeps multi-group aggregate cards unavailable when summary is missing', async () => {
+ const groups: PerformanceGroup[] = [
+ {
+ group: 'small',
+ avg_ttft_ms: 1000,
+ avg_latency_ms: 1000,
+ success_rate: 0,
+ avg_tps: 10,
+ series: [],
+ },
+ {
+ group: 'large',
+ avg_ttft_ms: 50,
+ avg_latency_ms: 100,
+ success_rate: 100,
+ avg_tps: 100,
+ series: [],
+ },
+ ]
+ const view = await testEnv.render(
+
+ )
+
+ try {
+ const detailText = view.container.textContent ?? ''
+ assert.ok(detailText.includes('N/A'))
+ assert.ok(!detailText.includes('No incidents in the last 24 hours'))
+ assert.ok(detailText.includes('small'))
+ assert.ok(detailText.includes('large'))
+ } finally {
+ await view.unmount()
+ }
+ })
+
+ test('expands the uptime axis to include severe availability failures', () => {
+ assert.deepEqual(
+ getUptimeAxisDomain([
+ {
+ date: new Date(3600 * 1000).toISOString(),
+ uptime_pct: 4.11,
+ incidents: 1,
+ outage_minutes: 0,
+ },
+ {
+ date: new Date(7200 * 1000).toISOString(),
+ uptime_pct: 99.5,
+ incidents: 1,
+ outage_minutes: 0,
+ },
+ ]),
+ { min: 0, max: 100 }
+ )
+ })
+
+ test('keeps minute-level chart categories unique', () => {
+ const first = new Date('2026-08-18T10:01:00.000Z').toISOString()
+ const second = new Date('2026-08-18T10:02:00.000Z').toISOString()
+
+ const latency = buildLatencyChartData([
+ { timestamp: first, group: 'default', ttft_ms: 100 },
+ { timestamp: second, group: 'default', ttft_ms: 120 },
+ ])
+ const uptime = buildUptimeChartData([
+ { date: first, uptime_pct: 99, incidents: 1, outage_minutes: 0 },
+ { date: second, uptime_pct: 100, incidents: 0, outage_minutes: 0 },
+ ])
+
+ assert.notEqual(latency[0].time, latency[1].time)
+ assert.notEqual(uptime[0].date, uptime[1].date)
+ })
+
+ test('downsamples dense uptime series while preserving the worst point in each window', () => {
+ const series = Array.from({ length: 100 }, (_, index) => ({
+ date: new Date((index + 1) * 60_000).toISOString(),
+ uptime_pct: index === 49 ? 12.5 : 100,
+ incidents: index === 49 ? 1 : 0,
+ outage_minutes: 0,
+ }))
+
+ const sampled = downsampleUptimeSeries(series, 32)
+
+ assert.equal(sampled.length, 32)
+ assert.ok(sampled.some((point) => point.uptime_pct === 12.5))
+ })
+
+ test('uses the weighted overall uptime supplied by the backend', async () => {
+ const view = await testEnv.render(
+
+ )
+
+ try {
+ assert.ok(view.container.textContent?.includes('99.0%'))
+ assert.ok(!view.container.textContent?.includes('50.0%'))
+ } finally {
+ await view.unmount()
+ }
+ })
+})
diff --git a/web/default/src/features/smart-ops/model-performance.tsx b/web/default/src/features/smart-ops/model-performance.tsx
new file mode 100644
index 00000000..82c9b2c3
--- /dev/null
+++ b/web/default/src/features/smart-ops/model-performance.tsx
@@ -0,0 +1,1006 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { useMemo, useState } from 'react'
+import { useMutation } from '@tanstack/react-query'
+import type { TFunction } from 'i18next'
+import {
+ AlertTriangle,
+ ArrowDown,
+ ArrowUp,
+ Bot,
+ Clock3,
+ DatabaseZap,
+ HeartPulse,
+ RefreshCw,
+ Search,
+ ShieldCheck,
+} from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { formatQuota } from '@/lib/format'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import {
+ Card,
+ CardAction,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu'
+import {
+ Empty,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyMedia,
+ EmptyTitle,
+} from '@/components/ui/empty'
+import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
+import { Input } from '@/components/ui/input'
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from '@/components/ui/sheet'
+import { Skeleton } from '@/components/ui/skeleton'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import { ErrorState } from '@/components/error-state'
+import { SectionPageLayout } from '@/components/layout'
+import {
+ formatThroughput,
+ formatUptimePct,
+} from '@/features/performance-metrics/lib/format'
+import type {
+ PerformanceCollectionState,
+ PerformanceCoverage,
+} from '@/features/performance-metrics/types'
+import { ModelPerformanceDetailsContent } from '@/features/pricing/components/model-details-performance'
+import { getModelPerformance, getModelPerformanceDetail } from './api'
+import {
+ DEFAULT_MODEL_FILTERS,
+ MAX_PERFORMANCE_HOURS,
+ normalizeFilters,
+ toModelQuery,
+ type ModelFilterDraft,
+} from './lib/filters'
+import { formatLegacyLatency } from './lib/format'
+import {
+ sortModelPerformanceItems,
+ type ModelPerformanceSortKey,
+ type ModelPerformanceSortState,
+} from './lib/model-sort'
+import { getAriaSort } from './lib/sort-direction'
+import { SortDirectionIcon } from './lib/sort-direction-icon'
+import type { ModelPerformanceItem } from './types'
+
+const priorityQualityFlags = [
+ 'error_logs_disabled',
+ 'consume_logs_disabled',
+ 'retry_backfill_pending',
+ 'retry_metrics_unavailable',
+ 'throughput_collection_disabled',
+ 'throughput_no_samples',
+ 'throughput_partial',
+ 'throughput_query_failed',
+ 'throughput_window_approximate',
+ 'time_range_clamped',
+ 'truncated',
+ 'incomplete_errors_possible',
+]
+
+function getVisibleQualityFlags(flags: string[]) {
+ const flagSet = new Set(flags)
+ const priorityFlags = priorityQualityFlags.filter((flag) => flagSet.has(flag))
+ const otherFlags = flags.filter(
+ (flag) => !priorityQualityFlags.includes(flag)
+ )
+ return [
+ ...priorityFlags,
+ ...otherFlags.slice(0, Math.max(0, 4 - priorityFlags.length)),
+ ]
+}
+
+export function ModelPerformanceCollectionStateAlert({
+ collectionState,
+}: {
+ collectionState: PerformanceCollectionState | undefined
+}) {
+ const { t } = useTranslation()
+
+ if (collectionState === 'collection_disabled') {
+ return (
+
+
+
+ {t('Performance metrics collection is disabled')}
+
+
+ {t(
+ 'New performance samples are not being collected. Existing performance data may be incomplete.'
+ )}
+
+
+ )
+ }
+
+ if (collectionState === 'no_samples') {
+ return (
+
+
+ {t('No throughput samples')}
+
+ {t('No performance samples were recorded for this time range.')}
+
+
+ )
+ }
+
+ if (collectionState === 'partial') {
+ return (
+
+
+ {t('Throughput data is partial')}
+
+ {t(
+ 'Stored and local performance samples are shown, but shared active buckets could not be read. Recent multi-node throughput may be incomplete.'
+ )}
+
+
+ )
+ }
+
+ if (collectionState === 'query_failed') {
+ return (
+
+
+ {t('Throughput query failed')}
+
+ {t(
+ 'Performance metrics could not be queried. Log-based model results remain available; check the application database.'
+ )}
+
+
+ )
+ }
+
+ return null
+}
+
+export function ModelPerformance() {
+ const { t, i18n } = useTranslation()
+ const [draft, setDraft] = useState(DEFAULT_MODEL_FILTERS)
+ const [filters, setFilters] = useState(
+ DEFAULT_MODEL_FILTERS
+ )
+ const [selected, setSelected] = useState(null)
+ const [sortState, setSortState] = useState(null)
+ const queryParams = useMemo(() => toModelQuery(filters), [filters])
+ const numberFormatter = useMemo(
+ () => new Intl.NumberFormat(i18n.language),
+ [i18n.language]
+ )
+ const dateFormatter = useMemo(
+ () =>
+ new Intl.DateTimeFormat(i18n.language, {
+ dateStyle: 'medium',
+ timeStyle: 'medium',
+ }),
+ [i18n.language]
+ )
+
+ const performanceQuery = useMutation({
+ mutationKey: ['smart-ops', 'model-performance'],
+ mutationFn: getModelPerformance,
+ retry: false,
+ })
+ const detailQuery = useMutation({
+ mutationKey: ['smart-ops', 'model-performance-detail'],
+ mutationFn: getModelPerformanceDetail,
+ retry: false,
+ })
+
+ const data = performanceQuery.data
+ const sortedItems = useMemo(
+ () =>
+ sortModelPerformanceItems(data?.items ?? [], sortState, i18n.language),
+ [data?.items, i18n.language, sortState]
+ )
+ const latestObservedAt = data?.summary.last_observed_at ?? 0
+ const retryBackfillPending =
+ data?.quality_flags.includes('retry_backfill_pending') ?? false
+ const retryMetricsUnavailable =
+ data?.quality_flags.includes('retry_metrics_unavailable') ?? false
+ const errorLogsDisabled =
+ data?.quality_flags.includes('error_logs_disabled') ?? false
+ const throughputWindowApproximate =
+ data?.quality_flags.includes('throughput_window_approximate') ?? false
+
+ const openDetails = (item: ModelPerformanceItem) => {
+ detailQuery.reset()
+ setSelected(item)
+ detailQuery.mutate(item.model_name)
+ }
+
+ const formatTimestamp = (timestamp: number | null | undefined) => {
+ if (!timestamp) return '—'
+ return dateFormatter.format(new Date(timestamp * 1000))
+ }
+
+ const qualityLabel = (flag: string) => {
+ const labels: Record = {
+ legacy: t('Legacy log'),
+ partial: t('Partial data'),
+ weak_correlation: t('Weak correlation'),
+ heuristic_outcome: t('Heuristic outcome'),
+ coarse_latency: t('Coarse latency'),
+ non_attempt_logs_excluded: t('Known non-attempt logs excluded'),
+ time_range_clamped: t('Time range was limited'),
+ truncated: t('Results truncated'),
+ retry_backfill_pending: t('Retry marker backfill pending'),
+ retry_metrics_unavailable: t('Retry metrics unavailable'),
+ consume_logs_disabled: t('Consumption logs disabled'),
+ error_logs_disabled: t('Error logs disabled'),
+ incomplete_errors_possible: t('Errors may be incomplete'),
+ throughput_collection_disabled: t(
+ 'Performance metrics collection is disabled'
+ ),
+ throughput_no_samples: t('No throughput samples'),
+ throughput_partial: t('Throughput data is partial'),
+ throughput_query_failed: t('Throughput query failed'),
+ throughput_window_approximate: t('Bucket-level estimate'),
+ }
+ return labels[flag] ?? flag
+ }
+
+ let summarySuccessRate = '—'
+ if (data) {
+ summarySuccessRate =
+ data.summary.observed_success_rate == null
+ ? t('N/A')
+ : formatUptimePct(data.summary.observed_success_rate)
+ }
+ const showPerformanceError =
+ !performanceQuery.isPending && performanceQuery.isError
+ const showPerformanceIdle =
+ !performanceQuery.isPending &&
+ !performanceQuery.isError &&
+ performanceQuery.isIdle
+ const showPerformanceEmpty =
+ !performanceQuery.isPending &&
+ !performanceQuery.isError &&
+ !performanceQuery.isIdle &&
+ !data?.items.length
+ const showPerformanceTable =
+ !performanceQuery.isPending &&
+ !performanceQuery.isError &&
+ !performanceQuery.isIdle &&
+ Boolean(data?.items.length)
+ const showDetailError = !detailQuery.isPending && detailQuery.isError
+ const showDetailData =
+ !detailQuery.isPending && !detailQuery.isError && detailQuery.data != null
+
+ return (
+
+
+
+ {t('Model performance')}
+ {t('Read only')}
+ {t('Legacy log')}
+
+
+
+
+
+
+
+
+
+ {t('Log fallback mode')}
+
+ {t(
+ 'This legacy view aggregates estimated model outcomes from eligible Consume and Error logs. It is not a complete Relay Attempt trace, and logged latency remains accurate only to whole seconds.'
+ )}
+
+
+
+ {(retryBackfillPending || retryMetricsUnavailable) && (
+
+
+
+ {t('Retry and latency data are temporarily unavailable')}
+
+
+ {retryBackfillPending
+ ? t(
+ 'Historical retry markers are still being prepared. Request counts and estimated success rates remain available; retry counts and logged latency will appear after the backfill completes.'
+ )
+ : t(
+ 'The retry marker readiness state could not be verified. Request counts and estimated success rates remain available, but retry counts and logged latency are unavailable; check the application and log databases.'
+ )}
+
+
+ )}
+
+ {errorLogsDisabled && (
+
+
+ {t('Error log collection is disabled')}
+
+ {t(
+ 'The displayed error count includes only recorded logs and may be incomplete. Estimated success rate is unavailable while error log collection is disabled.'
+ )}
+
+
+ )}
+
+
+
+ {throughputWindowApproximate && data?.throughput?.coverage && (
+
+
+ {t('Bucket-level estimate')}
+
+ {formatCoverageDescription(
+ t,
+ data.throughput.coverage,
+ formatTimestamp
+ )}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {performanceQuery.isPending && }
+ {showPerformanceError && (
+ performanceQuery.mutate(queryParams)}
+ />
+ )}
+ {showPerformanceIdle && (
+
+
+
+
+
+
+ {t('Model performance is not loaded automatically')}
+
+
+ {t(
+ 'Select Apply filters or Refresh to query the current time range.'
+ )}
+
+
+
+ )}
+ {showPerformanceEmpty && (
+
+
+
+
+
+ {t('No recorded model logs')}
+
+ {t(
+ 'Try a wider time range or check whether consumption and error logging are enabled.'
+ )}
+
+
+
+ )}
+ {showPerformanceTable && data && (
+
.
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+import type { TopupStatus } from '../types'
+import { getStatusConfig, isCompletableTopupStatus } from './billing'
+
+describe('top-up billing status', () => {
+ test('shows paid reconciliation orders explicitly', () => {
+ assert.deepEqual(getStatusConfig('paid_reconciliation' as TopupStatus), {
+ variant: 'warning',
+ label: 'Paid - Needs Reconciliation',
+ })
+ })
+
+ test('allows admins to complete pending and reconciliation orders only', () => {
+ assert.equal(isCompletableTopupStatus('pending'), true)
+ assert.equal(isCompletableTopupStatus('paid_reconciliation'), true)
+ assert.equal(isCompletableTopupStatus('success'), false)
+ assert.equal(isCompletableTopupStatus('failed'), false)
+ assert.equal(isCompletableTopupStatus('expired'), false)
+ })
+})
diff --git a/web/default/src/features/wallet/lib/billing.ts b/web/default/src/features/wallet/lib/billing.ts
index 0df69d90..a8711aee 100644
--- a/web/default/src/features/wallet/lib/billing.ts
+++ b/web/default/src/features/wallet/lib/billing.ts
@@ -41,10 +41,18 @@ export const STATUS_CONFIG: Record = {
variant: 'warning',
label: 'Pending',
},
+ failed: {
+ variant: 'danger',
+ label: 'Failed',
+ },
expired: {
variant: 'danger',
label: 'Expired',
},
+ paid_reconciliation: {
+ variant: 'warning',
+ label: 'Paid - Needs Reconciliation',
+ },
}
/**
@@ -54,6 +62,10 @@ export function getStatusConfig(status: TopupStatus): StatusConfig {
return STATUS_CONFIG[status] || STATUS_CONFIG.pending
}
+export function isCompletableTopupStatus(status: TopupStatus): boolean {
+ return status === 'pending' || status === 'paid_reconciliation'
+}
+
/**
* Payment method display names
*/
diff --git a/web/default/src/features/wallet/types.ts b/web/default/src/features/wallet/types.ts
index 930928ee..2c44dd8a 100644
--- a/web/default/src/features/wallet/types.ts
+++ b/web/default/src/features/wallet/types.ts
@@ -247,7 +247,12 @@ export interface UserWalletData {
/**
* Topup record status
*/
-export type TopupStatus = 'success' | 'pending' | 'expired'
+export type TopupStatus =
+ | 'success'
+ | 'pending'
+ | 'failed'
+ | 'expired'
+ | 'paid_reconciliation'
/**
* Topup billing record
diff --git a/web/default/src/hooks/use-sidebar-config.ts b/web/default/src/hooks/use-sidebar-config.ts
index 8c013279..e42209a9 100644
--- a/web/default/src/hooks/use-sidebar-config.ts
+++ b/web/default/src/hooks/use-sidebar-config.ts
@@ -62,6 +62,7 @@ const DEFAULT_SIDEBAR_MODULES: SidebarModulesAdminConfig = {
user: true,
setting: true,
subscription: true,
+ smart_ops: true,
},
}
@@ -113,9 +114,27 @@ const URL_TO_CONFIG_MAP: Record = {
'/users': { section: 'admin', module: 'user' },
'/redemption-codes': { section: 'admin', module: 'redemption' },
'/subscriptions': { section: 'admin', module: 'subscription' },
+ '/smart-ops': { section: 'admin', module: 'smart_ops' },
+ '/smart-ops/alerts': { section: 'admin', module: 'smart_ops' },
+ '/smart-ops/production-performance': {
+ section: 'admin',
+ module: 'smart_ops',
+ },
+ '/smart-ops/channel-performance': {
+ section: 'admin',
+ module: 'smart_ops',
+ },
+ '/smart-ops/model-performance': {
+ section: 'admin',
+ module: 'smart_ops',
+ },
+ '/smart-ops/system-info': {
+ section: 'admin',
+ module: 'smart_ops',
+ },
'/system-settings': { section: 'admin', module: 'setting' },
'/system-settings/site': { section: 'admin', module: 'setting' },
- '/system-info': { section: 'admin', module: 'setting' },
+ '/system-info': { section: 'admin', module: 'smart_ops' },
}
/**
diff --git a/web/default/src/hooks/use-sidebar-data.ts b/web/default/src/hooks/use-sidebar-data.ts
index c930f8e2..9eb21aca 100644
--- a/web/default/src/hooks/use-sidebar-data.ts
+++ b/web/default/src/hooks/use-sidebar-data.ts
@@ -22,12 +22,12 @@ import {
CreditCard,
FileText,
FlaskConical,
+ HeartPulse,
Key,
LayoutDashboard,
ListTodo,
MessageSquare,
Radio,
- ServerCog,
Settings,
Ticket,
User,
@@ -35,8 +35,6 @@ import {
Wallet,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
-import { useAuthStore } from '@/stores/auth-store'
-import { ROLE } from '@/lib/roles'
import { type SidebarData } from '@/components/layout/types'
/**
@@ -47,8 +45,6 @@ import { type SidebarData } from '@/components/layout/types'
*/
export function useSidebarData(): SidebarData {
const { t } = useTranslation()
- const user = useAuthStore((state) => state.auth.user)
- const isSuperAdmin = user?.role === ROLE.SUPER_ADMIN
return {
navGroups: [
@@ -146,21 +142,18 @@ export function useSidebarData(): SidebarData {
url: '/subscriptions',
icon: CreditCard,
},
+ {
+ title: t('Smart Operations'),
+ url: '/smart-ops/alerts',
+ activeUrls: ['/smart-ops'],
+ icon: HeartPulse,
+ },
{
title: t('System Settings'),
url: '/system-settings/site',
activeUrls: ['/system-settings'],
icon: Settings,
},
- ...(isSuperAdmin
- ? [
- {
- title: t('System Info'),
- url: '/system-info',
- icon: ServerCog,
- },
- ]
- : []),
],
},
],
diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json
index 4e110c67..08d8bb50 100644
--- a/web/default/src/i18n/locales/en.json
+++ b/web/default/src/i18n/locales/en.json
@@ -3407,6 +3407,7 @@
"Peak": "Peak",
"Peak throughput": "Peak throughput",
"Penalises repetition of frequent tokens": "Penalises repetition of frequent tokens",
+ "Paid - Needs Reconciliation": "Paid - Needs Reconciliation",
"Pending": "Pending",
"per": "per",
"Per 1K tokens": "Per 1K tokens",
@@ -5354,6 +5355,110 @@
"Zero retention": "Zero retention",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",
- "Zoom": "Zoom"
+ "Zoom": "Zoom",
+ "Smart Operations": "Smart Operations",
+ "Smart Operations Center": "Smart Operations Center",
+ "Production performance": "Production performance",
+ "Production performance is not loaded automatically": "Production performance is not loaded automatically",
+ "Select Apply filters or Refresh to query the current time range.": "Select Apply filters or Refresh to query the current time range.",
+ "Read only": "Read only",
+ "Legacy log": "Legacy log",
+ "Log fallback mode": "Log fallback mode",
+ "This legacy view estimates outcomes from eligible Consume and Error logs. Known billing adjustments and channel probes are excluded, but it is not a complete Relay Attempt trace. Logged latency uses only non-retry samples and remains accurate only to whole seconds.": "This legacy view estimates outcomes from eligible Consume and Error logs. Known billing adjustments and channel probes are excluded, but it is not a complete Relay Attempt trace. Logged latency uses only non-retry samples and remains accurate only to whole seconds.",
+ "Retry and latency data are temporarily unavailable": "Retry and latency data are temporarily unavailable",
+ "Historical retry markers are still being prepared. Request counts and estimated success rates remain available; retry counts and logged latency will appear after the backfill completes.": "Historical retry markers are still being prepared. Request counts and estimated success rates remain available; retry counts and logged latency will appear after the backfill completes.",
+ "The retry marker readiness state could not be verified. Request counts and estimated success rates remain available, but retry counts and logged latency are unavailable; check the application and log databases.": "The retry marker readiness state could not be verified. Request counts and estimated success rates remain available, but retry counts and logged latency are unavailable; check the application and log databases.",
+ "Time range": "Time range",
+ "All channels": "All channels",
+ "All models": "All models",
+ "Apply filters": "Apply filters",
+ "Observed log events": "Observed log events",
+ "Consume logs": "Consume logs",
+ "Consumed quota": "Consumed quota",
+ "Estimated success rate": "Estimated success rate",
+ "Latency samples": "Latency samples",
+ "Last observed": "Last observed",
+ "Unable to load production performance": "Unable to load production performance",
+ "Check log database availability and administrator permissions, then try again.": "Check log database availability and administrator permissions, then try again.",
+ "No recorded production logs": "No recorded production logs",
+ "Try a wider time range or check whether consumption and error logging are enabled.": "Try a wider time range or check whether consumption and error logging are enabled.",
+ "{{count}} rows": "{{count}} rows",
+ "The table was truncated to {{count}} rows. Summary cards still cover all matching eligible logs.": "The table was truncated to {{count}} rows. Summary cards still cover all matching eligible logs.",
+ "Generated at {{time}}": "Generated at {{time}}",
+ "Production performance details": "Production performance details",
+ "Channel type": "Channel type",
+ "Error logs": "Error logs",
+ "Logged latency": "Logged latency",
+ "Retries": "Retries",
+ "Probe latency": "Probe latency",
+ "Data quality": "Data quality",
+ "Interpret with caution": "Interpret with caution",
+ "Probe latency is shown for comparison only and is not included in production log latency or success-rate calculations.": "Probe latency is shown for comparison only and is not included in production log latency or success-rate calculations.",
+ "Partial data": "Partial data",
+ "Weak correlation": "Weak correlation",
+ "Heuristic outcome": "Heuristic outcome",
+ "Coarse latency": "Coarse latency",
+ "Probe data is separate": "Probe data is separate",
+ "Known non-attempt logs excluded": "Known non-attempt logs excluded",
+ "Time range was limited": "Time range was limited",
+ "Results truncated": "Results truncated",
+ "Retry marker backfill pending": "Retry marker backfill pending",
+ "Retry metrics unavailable": "Retry metrics unavailable",
+ "Consumption logs disabled": "Consumption logs disabled",
+ "Error logs disabled": "Error logs disabled",
+ "Error log collection is disabled": "Error log collection is disabled",
+ "Errors may be incomplete": "Errors may be incomplete",
+ "Recorded error logs": "Recorded error logs",
+ "The displayed error count includes only recorded logs and may be incomplete. Estimated success rate is unavailable while error log collection is disabled.": "The displayed error count includes only recorded logs and may be incomplete. Estimated success rate is unavailable while error log collection is disabled.",
+ "Channel metadata missing": "Channel metadata missing",
+ "Probe unavailable": "Probe unavailable",
+ "Channel performance": "Channel performance",
+ "Channel performance details": "Channel performance details",
+ "Channel performance is not loaded automatically": "Channel performance is not loaded automatically",
+ "No recorded channel logs": "No recorded channel logs",
+ "Unable to load channel performance": "Unable to load channel performance",
+ "Model performance": "Model performance",
+ "Observed model log events": "Observed model log events",
+ "Unable to load model performance": "Unable to load model performance",
+ "Model performance is not loaded automatically": "Model performance is not loaded automatically",
+ "No recorded model logs": "No recorded model logs",
+ "Model performance details": "Model performance details",
+ "This legacy view aggregates estimated model outcomes from eligible Consume and Error logs. It is not a complete Relay Attempt trace, and logged latency remains accurate only to whole seconds.": "This legacy view aggregates estimated model outcomes from eligible Consume and Error logs. It is not a complete Relay Attempt trace, and logged latency remains accurate only to whole seconds.",
+ "Model results are aggregated from eligible logs and are not a complete Relay Attempt trace.": "Model results are aggregated from eligible logs and are not a complete Relay Attempt trace.",
+ "Inspect read-only production performance and operational evidence.": "Inspect read-only production performance and operational evidence.",
+ "Throughput unavailable": "Throughput unavailable",
+ "Model throughput could not be read from performance metrics. Other log-based results remain available; check performance metrics collection and database availability.": "Model throughput could not be read from performance metrics. Other log-based results remain available; check performance metrics collection and database availability.",
+ "Unable to load model performance details": "Unable to load model performance details",
+ "Check performance metrics collection and database availability, then try again.": "Check performance metrics collection and database availability, then try again.",
+ "Bucket-level estimate": "Bucket-level estimate",
+ "New performance samples are not being collected. Existing performance data may be incomplete.": "New performance samples are not being collected. Existing performance data may be incomplete.",
+ "No performance samples were recorded for this time range.": "No performance samples were recorded for this time range.",
+ "No throughput samples": "No throughput samples",
+ "Performance metrics collection is disabled": "Performance metrics collection is disabled",
+ "Performance metrics could not be queried. Log-based model results remain available; check the application database.": "Performance metrics could not be queried. Log-based model results remain available; check the application database.",
+ "Throughput coverage: {{start}} – {{end}} ({{minutes}} minute buckets).": "Throughput coverage: {{start}} – {{end}} ({{minutes}} minute buckets).",
+ "Throughput coverage: {{start}} – {{end}}. Bucket granularity is unknown or mixed, so this range is approximate.": "Throughput coverage: {{start}} – {{end}}. Bucket granularity is unknown or mixed, so this range is approximate.",
+ "Throughput query failed": "Throughput query failed",
+ "Last 24 hours": "Last 24 hours",
+ "Channel models and groups from eligible production logs": "Channel models and groups from eligible production logs",
+ "Unable to load channel performance details": "Unable to load channel performance details",
+ "No channel model performance": "No channel model performance",
+ "No eligible production logs were recorded for this channel in the last 24 hours.": "No eligible production logs were recorded for this channel in the last 24 hours.",
+ "The detail table was truncated to {{count}} rows. The summary still covers all matching eligible logs.": "The detail table was truncated to {{count}} rows. The summary still covers all matching eligible logs.",
+ "Throughput data is partial": "Throughput data is partial",
+ "Stored and local performance samples are shown, but shared active buckets could not be read. Recent multi-node throughput may be incomplete.": "Stored and local performance samples are shown, but shared active buckets could not be read. Recent multi-node throughput may be incomplete.",
+ "Active Alerts": "Active Alerts",
+ "Alerts requiring administrator attention": "Alerts requiring administrator attention",
+ "SmartOps reports sustained host pressure here for manual investigation and repair.": "SmartOps reports sustained host pressure here for manual investigation and repair.",
+ "Resource": "Resource",
+ "Threshold": "Threshold",
+ "Observed": "Observed",
+ "CPU usage": "CPU usage",
+ "Memory usage": "Memory usage",
+ "Disk usage": "Disk usage",
+ "Firing": "Firing",
+ "We could not load active alerts.": "We could not load active alerts.",
+ "No active alerts.": "No active alerts.",
+ "This process is not currently reporting a sustained CPU, memory, or disk alert.": "This process is not currently reporting a sustained CPU, memory, or disk alert."
}
}
diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json
index 24c7fc4d..a6bcc32a 100644
--- a/web/default/src/i18n/locales/fr.json
+++ b/web/default/src/i18n/locales/fr.json
@@ -3407,6 +3407,7 @@
"Peak": "Pic",
"Peak throughput": "Débit de pointe",
"Penalises repetition of frequent tokens": "Pénalise la répétition des jetons fréquents",
+ "Paid - Needs Reconciliation": "Payé - Rapprochement requis",
"Pending": "En attente",
"per": "par",
"Per 1K tokens": "Par 1K tokens",
@@ -5354,6 +5355,110 @@
"Zero retention": "Aucune rétention",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",
- "Zoom": "Zoom"
+ "Zoom": "Zoom",
+ "Smart Operations": "Opérations intelligentes",
+ "Smart Operations Center": "Centre des opérations intelligentes",
+ "Production performance": "Performances de production",
+ "Production performance is not loaded automatically": "Les performances de production ne sont pas chargées automatiquement",
+ "Select Apply filters or Refresh to query the current time range.": "Sélectionnez Appliquer les filtres ou Actualiser pour interroger la période actuelle.",
+ "Read only": "Lecture seule",
+ "Legacy log": "Journal hérité",
+ "Log fallback mode": "Mode de repli des journaux",
+ "This legacy view estimates outcomes from eligible Consume and Error logs. Known billing adjustments and channel probes are excluded, but it is not a complete Relay Attempt trace. Logged latency uses only non-retry samples and remains accurate only to whole seconds.": "Cette vue historique estime les résultats à partir des journaux Consume et Error éligibles. Les ajustements de facturation connus et les sondes de canal sont exclus, mais elle ne constitue pas une trace Relay Attempt complète. La latence journalisée utilise uniquement les échantillons sans nouvelle tentative et reste précise à la seconde entière seulement.",
+ "Retry and latency data are temporarily unavailable": "Les données de nouvelle tentative et de latence sont temporairement indisponibles",
+ "Historical retry markers are still being prepared. Request counts and estimated success rates remain available; retry counts and logged latency will appear after the backfill completes.": "Les marqueurs historiques de nouvelle tentative sont encore en cours de préparation. Le nombre de requêtes et les taux de réussite estimés restent disponibles ; le nombre de nouvelles tentatives et la latence journalisée apparaîtront une fois le remplissage terminé.",
+ "The retry marker readiness state could not be verified. Request counts and estimated success rates remain available, but retry counts and logged latency are unavailable; check the application and log databases.": "L'état de préparation des marqueurs de nouvelle tentative n'a pas pu être vérifié. Le nombre de requêtes et les taux de réussite estimés restent disponibles, mais le nombre de nouvelles tentatives et la latence journalisée sont indisponibles ; vérifiez les bases de données de l'application et des journaux.",
+ "Time range": "Période",
+ "All channels": "Tous les canaux",
+ "All models": "Tous les modèles",
+ "Apply filters": "Appliquer les filtres",
+ "Observed log events": "Événements de journal observés",
+ "Consume logs": "Journaux Consume",
+ "Consumed quota": "Quota consommé",
+ "Estimated success rate": "Taux de réussite estimé",
+ "Latency samples": "Échantillons de latence",
+ "Last observed": "Dernière observation",
+ "Unable to load production performance": "Impossible de charger les performances de production",
+ "Check log database availability and administrator permissions, then try again.": "Vérifiez la disponibilité de la base de journaux et les droits administrateur, puis réessayez.",
+ "No recorded production logs": "Aucun journal de production enregistré",
+ "Try a wider time range or check whether consumption and error logging are enabled.": "Élargissez la période ou vérifiez que les journaux de consommation et d'erreur sont activés.",
+ "{{count}} rows": "{{count}} lignes",
+ "The table was truncated to {{count}} rows. Summary cards still cover all matching eligible logs.": "Le tableau a été tronqué à {{count}} lignes. Les cartes récapitulatives couvrent toujours tous les journaux éligibles correspondants.",
+ "Generated at {{time}}": "Généré à {{time}}",
+ "Production performance details": "Détails des performances de production",
+ "Channel type": "Type de canal",
+ "Error logs": "Journaux d'erreur",
+ "Logged latency": "Latence journalisée",
+ "Retries": "Nouvelles tentatives",
+ "Probe latency": "Latence de sonde",
+ "Data quality": "Qualité des données",
+ "Interpret with caution": "À interpréter avec prudence",
+ "Probe latency is shown for comparison only and is not included in production log latency or success-rate calculations.": "La latence de sonde est fournie à titre comparatif et n'entre pas dans les calculs de latence ou de taux de succès de production.",
+ "Partial data": "Données partielles",
+ "Weak correlation": "Corrélation faible",
+ "Heuristic outcome": "Résultat heuristique",
+ "Coarse latency": "Latence approximative",
+ "Probe data is separate": "Données de sonde séparées",
+ "Known non-attempt logs excluded": "Journaux connus hors tentative exclus",
+ "Time range was limited": "Période limitée",
+ "Results truncated": "Résultats tronqués",
+ "Retry marker backfill pending": "Remplissage des marqueurs de nouvelle tentative en attente",
+ "Retry metrics unavailable": "Métriques de nouvelle tentative indisponibles",
+ "Consumption logs disabled": "Journaux de consommation désactivés",
+ "Error logs disabled": "Journaux d'erreur désactivés",
+ "Error log collection is disabled": "La collecte des journaux d'erreur est désactivée",
+ "Errors may be incomplete": "Les erreurs peuvent être incomplètes",
+ "Recorded error logs": "Journaux d'erreur enregistrés",
+ "The displayed error count includes only recorded logs and may be incomplete. Estimated success rate is unavailable while error log collection is disabled.": "Le nombre d'erreurs affiché comprend uniquement les journaux enregistrés et peut être incomplet. Le taux de réussite estimé est indisponible tant que la collecte des journaux d'erreur est désactivée.",
+ "Channel metadata missing": "Métadonnées du canal manquantes",
+ "Probe unavailable": "Sonde indisponible",
+ "Channel performance": "Performance des canaux",
+ "Channel performance details": "Détails des performances des canaux",
+ "Channel performance is not loaded automatically": "Les performances des canaux ne sont pas chargées automatiquement",
+ "No recorded channel logs": "Aucun journal de canal enregistré",
+ "Unable to load channel performance": "Impossible de charger les performances des canaux",
+ "Model performance": "Performance des modèles",
+ "Observed model log events": "Événements de journal des modèles observés",
+ "Unable to load model performance": "Impossible de charger les performances des modèles",
+ "Model performance is not loaded automatically": "Les performances des modèles ne sont pas chargées automatiquement",
+ "No recorded model logs": "Aucun journal de modèle enregistré",
+ "Model performance details": "Détails des performances du modèle",
+ "This legacy view aggregates estimated model outcomes from eligible Consume and Error logs. It is not a complete Relay Attempt trace, and logged latency remains accurate only to whole seconds.": "Cette vue héritée agrège les résultats estimés des modèles à partir des journaux Consume et Error admissibles. Elle ne constitue pas une trace Relay Attempt complète et la latence journalisée n'est précise qu'à la seconde.",
+ "Model results are aggregated from eligible logs and are not a complete Relay Attempt trace.": "Les résultats des modèles sont agrégés à partir des journaux admissibles et ne constituent pas une trace Relay Attempt complète.",
+ "Inspect read-only production performance and operational evidence.": "Examiner les performances de production et les preuves opérationnelles en lecture seule.",
+ "Throughput unavailable": "Débit indisponible",
+ "Model throughput could not be read from performance metrics. Other log-based results remain available; check performance metrics collection and database availability.": "Le débit des modèles n'a pas pu être lu depuis les métriques de performance. Les autres résultats fondés sur les journaux restent disponibles ; vérifiez la collecte des métriques et la disponibilité de la base de données.",
+ "Unable to load model performance details": "Impossible de charger les détails des performances du modèle",
+ "Check performance metrics collection and database availability, then try again.": "Vérifiez la collecte des métriques de performance et la disponibilité de la base de données, puis réessayez.",
+ "Bucket-level estimate": "Estimation par compartiments",
+ "New performance samples are not being collected. Existing performance data may be incomplete.": "Aucun nouvel échantillon de performance n'est collecté. Les données existantes peuvent être incomplètes.",
+ "No performance samples were recorded for this time range.": "Aucun échantillon de performance n'a été enregistré pour cette période.",
+ "No throughput samples": "Aucun échantillon de débit",
+ "Performance metrics collection is disabled": "La collecte des métriques de performance est désactivée",
+ "Performance metrics could not be queried. Log-based model results remain available; check the application database.": "Les métriques de performance n'ont pas pu être interrogées. Les résultats fondés sur les journaux restent disponibles ; vérifiez la base de données de l'application.",
+ "Throughput coverage: {{start}} – {{end}} ({{minutes}} minute buckets).": "Couverture du débit : {{start}} – {{end}} (compartiments de {{minutes}} minutes).",
+ "Throughput coverage: {{start}} – {{end}}. Bucket granularity is unknown or mixed, so this range is approximate.": "Couverture du débit : {{start}} – {{end}}. La granularité des compartiments est inconnue ou mixte ; cette plage est donc approximative.",
+ "Throughput query failed": "Échec de la requête de débit",
+ "Last 24 hours": "Dernières 24 heures",
+ "Channel models and groups from eligible production logs": "Modèles et groupes du canal issus des journaux de production éligibles",
+ "Unable to load channel performance details": "Impossible de charger les détails des performances du canal",
+ "No channel model performance": "Aucune performance de modèle de canal",
+ "No eligible production logs were recorded for this channel in the last 24 hours.": "Aucun journal de production éligible n'a été enregistré pour ce canal au cours des dernières 24 heures.",
+ "The detail table was truncated to {{count}} rows. The summary still covers all matching eligible logs.": "Le tableau détaillé a été limité à {{count}} lignes. Le résumé couvre toujours tous les journaux éligibles correspondants.",
+ "Throughput data is partial": "Données de débit partielles",
+ "Stored and local performance samples are shown, but shared active buckets could not be read. Recent multi-node throughput may be incomplete.": "Les échantillons stockés et locaux sont affichés, mais les compartiments actifs partagés sont illisibles. Le débit multi-nœuds récent peut être incomplet.",
+ "Active Alerts": "Alertes actives",
+ "Alerts requiring administrator attention": "Alertes nécessitant l’attention d’un administrateur",
+ "SmartOps reports sustained host pressure here for manual investigation and repair.": "SmartOps signale ici les pressions persistantes sur l’hôte afin qu’elles soient examinées et corrigées manuellement.",
+ "Resource": "Ressource",
+ "Threshold": "Seuil",
+ "Observed": "Observé",
+ "CPU usage": "Utilisation du processeur",
+ "Memory usage": "Utilisation de la mémoire",
+ "Disk usage": "Utilisation du disque",
+ "Firing": "En cours",
+ "We could not load active alerts.": "Impossible de charger les alertes actives.",
+ "No active alerts.": "Aucune alerte active.",
+ "This process is not currently reporting a sustained CPU, memory, or disk alert.": "Ce processus ne signale actuellement aucune alerte persistante concernant le processeur, la mémoire ou le disque."
}
}
diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json
index c212ba80..7fc1337e 100644
--- a/web/default/src/i18n/locales/ja.json
+++ b/web/default/src/i18n/locales/ja.json
@@ -3407,6 +3407,7 @@
"Peak": "ピーク",
"Peak throughput": "ピークスループット",
"Penalises repetition of frequent tokens": "頻出トークンの繰り返しを抑制します",
+ "Paid - Needs Reconciliation": "支払い済み - 照合が必要",
"Pending": "保留中",
"per": "あたり",
"Per 1K tokens": "1Kトークンあたり",
@@ -5354,6 +5355,110 @@
"Zero retention": "データ保持なし",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V 4",
- "Zoom": "ズーム"
+ "Zoom": "ズーム",
+ "Smart Operations": "スマート運用",
+ "Smart Operations Center": "スマート運用センター",
+ "Production performance": "本番性能",
+ "Production performance is not loaded automatically": "本番性能は自動では読み込まれません",
+ "Select Apply filters or Refresh to query the current time range.": "「フィルターを適用」または「更新」を選択して、現在の期間を照会してください。",
+ "Read only": "読み取り専用",
+ "Legacy log": "レガシーログ",
+ "Log fallback mode": "ログフォールバックモード",
+ "This legacy view estimates outcomes from eligible Consume and Error logs. Known billing adjustments and channel probes are excluded, but it is not a complete Relay Attempt trace. Logged latency uses only non-retry samples and remains accurate only to whole seconds.": "このレガシービューは、対象となる Consume および Error ログから結果を推定します。既知の課金調整とチャネルプローブは除外していますが、完全な Relay Attempt トレースではありません。ログ遅延はリトライのないサンプルのみを使用し、精度は引き続き整数秒単位です。",
+ "Retry and latency data are temporarily unavailable": "リトライと遅延のデータは一時的に利用できません",
+ "Historical retry markers are still being prepared. Request counts and estimated success rates remain available; retry counts and logged latency will appear after the backfill completes.": "過去のリトライマーカーを準備しています。リクエスト数と推定成功率は引き続き利用でき、バックフィル完了後にリトライ回数とログ遅延が表示されます。",
+ "The retry marker readiness state could not be verified. Request counts and estimated success rates remain available, but retry counts and logged latency are unavailable; check the application and log databases.": "リトライマーカーの準備状態を確認できませんでした。リクエスト数と推定成功率は利用できますが、リトライ回数とログ遅延は利用できません。アプリケーションDBとログDBを確認してください。",
+ "Time range": "期間",
+ "All channels": "すべてのチャネル",
+ "All models": "すべてのモデル",
+ "Apply filters": "フィルターを適用",
+ "Observed log events": "観測ログイベント",
+ "Consume logs": "Consumeログ",
+ "Consumed quota": "消費クォータ",
+ "Estimated success rate": "推定成功率",
+ "Latency samples": "レイテンシーサンプル",
+ "Last observed": "最終観測",
+ "Unable to load production performance": "本番性能を読み込めません",
+ "Check log database availability and administrator permissions, then try again.": "ログデータベースの可用性と管理者権限を確認して、もう一度お試しください。",
+ "No recorded production logs": "記録済みの本番ログはありません",
+ "Try a wider time range or check whether consumption and error logging are enabled.": "期間を広げるか、消費ログとエラーログが有効か確認してください。",
+ "{{count}} rows": "{{count}} 行",
+ "The table was truncated to {{count}} rows. Summary cards still cover all matching eligible logs.": "テーブルは{{count}}行に切り詰められました。サマリーカードは一致する対象ログ全体を引き続き集計しています。",
+ "Generated at {{time}}": "生成日時:{{time}}",
+ "Production performance details": "本番性能の詳細",
+ "Channel type": "チャネル種別",
+ "Error logs": "エラーログ",
+ "Logged latency": "ログ遅延",
+ "Retries": "リトライ",
+ "Probe latency": "プローブ遅延",
+ "Data quality": "データ品質",
+ "Interpret with caution": "注意して解釈してください",
+ "Probe latency is shown for comparison only and is not included in production log latency or success-rate calculations.": "プローブ遅延は比較用であり、本番ログの遅延や成功率の計算には含まれません。",
+ "Partial data": "部分データ",
+ "Weak correlation": "相関が弱い",
+ "Heuristic outcome": "推定結果",
+ "Coarse latency": "粗い遅延",
+ "Probe data is separate": "プローブデータは別表示",
+ "Known non-attempt logs excluded": "既知の非試行ログを除外",
+ "Time range was limited": "期間が制限されています",
+ "Results truncated": "結果を切り詰め",
+ "Retry marker backfill pending": "リトライマーカーのバックフィルが保留中",
+ "Retry metrics unavailable": "リトライ指標を利用不可",
+ "Consumption logs disabled": "消費ログは無効です",
+ "Error logs disabled": "エラーログは無効です",
+ "Error log collection is disabled": "エラーログ収集が無効です",
+ "Errors may be incomplete": "エラーが不完全な可能性があります",
+ "Recorded error logs": "記録済みエラーログ",
+ "The displayed error count includes only recorded logs and may be incomplete. Estimated success rate is unavailable while error log collection is disabled.": "表示されるエラー数は記録済みログのみを含むため、不完全な可能性があります。エラーログ収集が無効な間は推定成功率を利用できません。",
+ "Channel metadata missing": "チャネルメタデータがありません",
+ "Probe unavailable": "プローブ利用不可",
+ "Channel performance": "チャネル性能",
+ "Channel performance details": "チャネル性能の詳細",
+ "Channel performance is not loaded automatically": "チャネル性能は自動的に読み込まれません",
+ "No recorded channel logs": "記録されたチャネルログはありません",
+ "Unable to load channel performance": "チャネル性能を読み込めません",
+ "Model performance": "モデル性能",
+ "Observed model log events": "観測モデルログイベント",
+ "Unable to load model performance": "モデル性能を読み込めません",
+ "Model performance is not loaded automatically": "モデル性能は自動的に読み込まれません",
+ "No recorded model logs": "記録済みのモデルログはありません",
+ "Model performance details": "モデル性能の詳細",
+ "This legacy view aggregates estimated model outcomes from eligible Consume and Error logs. It is not a complete Relay Attempt trace, and logged latency remains accurate only to whole seconds.": "このレガシービューは、対象となる Consume および Error ログからモデル結果を推定して集計します。完全な Relay Attempt トレースではなく、記録されたレイテンシーの精度は秒単位です。",
+ "Model results are aggregated from eligible logs and are not a complete Relay Attempt trace.": "モデル結果は対象ログから集計されたもので、完全な Relay Attempt トレースではありません。",
+ "Inspect read-only production performance and operational evidence.": "読み取り専用の本番性能と運用証拠を確認します。",
+ "Throughput unavailable": "スループットを利用できません",
+ "Model throughput could not be read from performance metrics. Other log-based results remain available; check performance metrics collection and database availability.": "パフォーマンスメトリクスからモデルのスループットを読み取れませんでした。ログベースの他の結果は利用できます。パフォーマンスメトリクスの収集とデータベースの可用性を確認してください。",
+ "Unable to load model performance details": "モデル性能の詳細を読み込めません",
+ "Check performance metrics collection and database availability, then try again.": "パフォーマンスメトリクスの収集とデータベースの可用性を確認してから、もう一度お試しください。",
+ "Bucket-level estimate": "時間バケット単位の推定",
+ "New performance samples are not being collected. Existing performance data may be incomplete.": "新しいパフォーマンスサンプルは収集されていません。既存のデータは不完全な可能性があります。",
+ "No performance samples were recorded for this time range.": "この期間にはパフォーマンスサンプルが記録されていません。",
+ "No throughput samples": "スループットサンプルなし",
+ "Performance metrics collection is disabled": "パフォーマンスメトリクスの収集は無効です",
+ "Performance metrics could not be queried. Log-based model results remain available; check the application database.": "パフォーマンスメトリクスを照会できませんでした。ログベースのモデル結果は利用できます。アプリケーションデータベースを確認してください。",
+ "Throughput coverage: {{start}} – {{end}} ({{minutes}} minute buckets).": "スループットの対象範囲:{{start}} – {{end}}({{minutes}}分バケット)。",
+ "Throughput coverage: {{start}} – {{end}}. Bucket granularity is unknown or mixed, so this range is approximate.": "スループットの対象範囲:{{start}} – {{end}}。バケット粒度が不明または混在しているため、この範囲は概算です。",
+ "Throughput query failed": "スループットの照会に失敗しました",
+ "Last 24 hours": "過去24時間",
+ "Channel models and groups from eligible production logs": "対象となる本番ログのチャネルモデルとグループ",
+ "Unable to load channel performance details": "チャネル性能の詳細を読み込めません",
+ "No channel model performance": "チャネルモデルの性能データはありません",
+ "No eligible production logs were recorded for this channel in the last 24 hours.": "過去24時間にこのチャネルの対象本番ログは記録されていません。",
+ "The detail table was truncated to {{count}} rows. The summary still covers all matching eligible logs.": "詳細テーブルは{{count}}行に制限されています。概要は一致する対象ログをすべてカバーします。",
+ "Throughput data is partial": "スループットデータが不完全です",
+ "Stored and local performance samples are shown, but shared active buckets could not be read. Recent multi-node throughput may be incomplete.": "保存済みおよびローカルの性能サンプルを表示していますが、共有アクティブバケットを読み取れませんでした。最近のマルチノードスループットは不完全な可能性があります。",
+ "Active Alerts": "アクティブなアラート",
+ "Alerts requiring administrator attention": "管理者の対応が必要なアラート",
+ "SmartOps reports sustained host pressure here for manual investigation and repair.": "SmartOps は、手動での調査と復旧のために、継続的なホスト負荷をここに報告します。",
+ "Resource": "リソース",
+ "Threshold": "しきい値",
+ "Observed": "観測時刻",
+ "CPU usage": "CPU 使用率",
+ "Memory usage": "メモリ使用率",
+ "Disk usage": "ディスク使用率",
+ "Firing": "発報中",
+ "We could not load active alerts.": "アクティブなアラートを読み込めませんでした。",
+ "No active alerts.": "アクティブなアラートはありません。",
+ "This process is not currently reporting a sustained CPU, memory, or disk alert.": "このプロセスは現在、継続的な CPU、メモリ、ディスクのアラートを報告していません。"
}
}
diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json
index aafa73d8..a7780eb7 100644
--- a/web/default/src/i18n/locales/ru.json
+++ b/web/default/src/i18n/locales/ru.json
@@ -3407,6 +3407,7 @@
"Peak": "Пик",
"Peak throughput": "Пиковая пропускная способность",
"Penalises repetition of frequent tokens": "Штрафует повторение частых токенов",
+ "Paid - Needs Reconciliation": "Оплачено — требуется сверка",
"Pending": "Ожидает",
"per": "за",
"Per 1K tokens": "За 1K токенов",
@@ -5354,6 +5355,110 @@
"Zero retention": "Без хранения данных",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",
- "Zoom": "Zoom"
+ "Zoom": "Zoom",
+ "Smart Operations": "Интеллектуальные операции",
+ "Smart Operations Center": "Центр интеллектуальных операций",
+ "Production performance": "Производительность в продакшене",
+ "Production performance is not loaded automatically": "Данные о производительности в продакшене не загружаются автоматически",
+ "Select Apply filters or Refresh to query the current time range.": "Нажмите «Применить фильтры» или «Обновить», чтобы запросить данные за текущий период.",
+ "Read only": "Только чтение",
+ "Legacy log": "Устаревший журнал",
+ "Log fallback mode": "Режим резервных журналов",
+ "This legacy view estimates outcomes from eligible Consume and Error logs. Known billing adjustments and channel probes are excluded, but it is not a complete Relay Attempt trace. Logged latency uses only non-retry samples and remains accurate only to whole seconds.": "Это представление на основе устаревших журналов оценивает результаты по подходящим записям Consume и Error. Известные корректировки биллинга и проверки каналов исключены, но это не полная трассировка Relay Attempt. Задержка по журналам использует только образцы без повторов, а ее точность по-прежнему ограничена целыми секундами.",
+ "Retry and latency data are temporarily unavailable": "Данные о повторах и задержке временно недоступны",
+ "Historical retry markers are still being prepared. Request counts and estimated success rates remain available; retry counts and logged latency will appear after the backfill completes.": "Исторические маркеры повторов еще подготавливаются. Количество запросов и расчетный показатель успешности остаются доступными; количество повторов и задержка по журналам появятся после завершения обратного заполнения.",
+ "The retry marker readiness state could not be verified. Request counts and estimated success rates remain available, but retry counts and logged latency are unavailable; check the application and log databases.": "Не удалось проверить готовность маркеров повторов. Количество запросов и расчетный показатель успешности остаются доступными, но количество повторов и задержка по журналам недоступны; проверьте базу приложения и базу журналов.",
+ "Time range": "Период",
+ "All channels": "Все каналы",
+ "All models": "Все модели",
+ "Apply filters": "Применить фильтры",
+ "Observed log events": "Наблюдаемые события журнала",
+ "Consume logs": "Записи Consume",
+ "Consumed quota": "Использованная квота",
+ "Estimated success rate": "Расчетная доля успеха",
+ "Latency samples": "Образцы задержки",
+ "Last observed": "Последнее наблюдение",
+ "Unable to load production performance": "Не удалось загрузить производительность в продакшене",
+ "Check log database availability and administrator permissions, then try again.": "Проверьте доступность базы журналов и права администратора, затем повторите попытку.",
+ "No recorded production logs": "Нет записанных производственных журналов",
+ "Try a wider time range or check whether consumption and error logging are enabled.": "Расширьте период или проверьте, включены ли журналы потребления и ошибок.",
+ "{{count}} rows": "Строк: {{count}}",
+ "The table was truncated to {{count}} rows. Summary cards still cover all matching eligible logs.": "Таблица усечена до {{count}} строк. Сводные карточки по-прежнему охватывают все подходящие совпавшие записи.",
+ "Generated at {{time}}": "Создано: {{time}}",
+ "Production performance details": "Подробности производительности в продакшене",
+ "Channel type": "Тип канала",
+ "Error logs": "Журналы ошибок",
+ "Logged latency": "Задержка по журналу",
+ "Retries": "Повторы",
+ "Probe latency": "Задержка проверки",
+ "Data quality": "Качество данных",
+ "Interpret with caution": "Интерпретируйте с осторожностью",
+ "Probe latency is shown for comparison only and is not included in production log latency or success-rate calculations.": "Задержка проверки показана только для сравнения и не входит в расчёты производственной задержки или доли успеха.",
+ "Partial data": "Неполные данные",
+ "Weak correlation": "Слабая корреляция",
+ "Heuristic outcome": "Эвристический результат",
+ "Coarse latency": "Приблизительная задержка",
+ "Probe data is separate": "Данные проверки отдельно",
+ "Known non-attempt logs excluded": "Известные записи, не являющиеся попытками, исключены",
+ "Time range was limited": "Период ограничен",
+ "Results truncated": "Результаты усечены",
+ "Retry marker backfill pending": "Ожидается обратное заполнение маркеров повторов",
+ "Retry metrics unavailable": "Метрики повторов недоступны",
+ "Consumption logs disabled": "Журналы потребления отключены",
+ "Error logs disabled": "Журналы ошибок отключены",
+ "Error log collection is disabled": "Сбор журналов ошибок отключен",
+ "Errors may be incomplete": "Ошибки могут быть неполными",
+ "Recorded error logs": "Записанные журналы ошибок",
+ "The displayed error count includes only recorded logs and may be incomplete. Estimated success rate is unavailable while error log collection is disabled.": "Показанное число ошибок включает только записанные журналы и может быть неполным. Расчетный показатель успешности недоступен, пока сбор журналов ошибок отключен.",
+ "Channel metadata missing": "Нет метаданных канала",
+ "Probe unavailable": "Данные проверки недоступны",
+ "Channel performance": "Производительность каналов",
+ "Channel performance details": "Сведения о производительности каналов",
+ "Channel performance is not loaded automatically": "Производительность каналов не загружается автоматически",
+ "No recorded channel logs": "Нет записанных журналов каналов",
+ "Unable to load channel performance": "Не удалось загрузить производительность каналов",
+ "Model performance": "Производительность моделей",
+ "Observed model log events": "Наблюдаемые события журналов моделей",
+ "Unable to load model performance": "Не удалось загрузить производительность моделей",
+ "Model performance is not loaded automatically": "Производительность моделей не загружается автоматически",
+ "No recorded model logs": "Нет записанных журналов моделей",
+ "Model performance details": "Сведения о производительности модели",
+ "This legacy view aggregates estimated model outcomes from eligible Consume and Error logs. It is not a complete Relay Attempt trace, and logged latency remains accurate only to whole seconds.": "Это устаревшее представление агрегирует расчетные результаты моделей по подходящим журналам Consume и Error. Оно не является полной трассировкой Relay Attempt, а задержка в журнале точна только до целых секунд.",
+ "Model results are aggregated from eligible logs and are not a complete Relay Attempt trace.": "Результаты моделей агрегируются по подходящим журналам и не являются полной трассировкой Relay Attempt.",
+ "Inspect read-only production performance and operational evidence.": "Просматривайте производительность и операционные свидетельства в режиме только чтения.",
+ "Throughput unavailable": "Пропускная способность недоступна",
+ "Model throughput could not be read from performance metrics. Other log-based results remain available; check performance metrics collection and database availability.": "Не удалось прочитать пропускную способность моделей из метрик производительности. Остальные результаты на основе журналов доступны; проверьте сбор метрик производительности и доступность базы данных.",
+ "Unable to load model performance details": "Не удалось загрузить сведения о производительности модели",
+ "Check performance metrics collection and database availability, then try again.": "Проверьте сбор метрик производительности и доступность базы данных, затем повторите попытку.",
+ "Bucket-level estimate": "Оценка по временным блокам",
+ "New performance samples are not being collected. Existing performance data may be incomplete.": "Новые образцы производительности не собираются. Существующие данные могут быть неполными.",
+ "No performance samples were recorded for this time range.": "За этот период образцы производительности не зарегистрированы.",
+ "No throughput samples": "Нет образцов пропускной способности",
+ "Performance metrics collection is disabled": "Сбор метрик производительности отключен",
+ "Performance metrics could not be queried. Log-based model results remain available; check the application database.": "Не удалось запросить метрики производительности. Результаты на основе журналов доступны; проверьте базу данных приложения.",
+ "Throughput coverage: {{start}} – {{end}} ({{minutes}} minute buckets).": "Охват пропускной способности: {{start}} – {{end}} (блоки по {{minutes}} мин.).",
+ "Throughput coverage: {{start}} – {{end}}. Bucket granularity is unknown or mixed, so this range is approximate.": "Охват пропускной способности: {{start}} – {{end}}. Гранулярность интервалов неизвестна или смешана, поэтому диапазон является приблизительным.",
+ "Throughput query failed": "Сбой запроса пропускной способности",
+ "Last 24 hours": "Последние 24 часа",
+ "Channel models and groups from eligible production logs": "Модели и группы канала по подходящим производственным журналам",
+ "Unable to load channel performance details": "Не удалось загрузить подробности производительности канала",
+ "No channel model performance": "Нет данных о производительности моделей канала",
+ "No eligible production logs were recorded for this channel in the last 24 hours.": "За последние 24 часа для этого канала не записано подходящих производственных журналов.",
+ "The detail table was truncated to {{count}} rows. The summary still covers all matching eligible logs.": "Таблица подробностей ограничена {{count}} строками. Сводка по-прежнему охватывает все подходящие журналы.",
+ "Throughput data is partial": "Данные о пропускной способности неполны",
+ "Stored and local performance samples are shown, but shared active buckets could not be read. Recent multi-node throughput may be incomplete.": "Показаны сохранённые и локальные образцы производительности, но общие активные сегменты не удалось прочитать. Последняя многонодовая пропускная способность может быть неполной.",
+ "Active Alerts": "Активные оповещения",
+ "Alerts requiring administrator attention": "Оповещения, требующие внимания администратора",
+ "SmartOps reports sustained host pressure here for manual investigation and repair.": "SmartOps сообщает здесь о длительной нагрузке на узел для ручной диагностики и устранения.",
+ "Resource": "Ресурс",
+ "Threshold": "Порог",
+ "Observed": "Время наблюдения",
+ "CPU usage": "Использование CPU",
+ "Memory usage": "Использование памяти",
+ "Disk usage": "Использование диска",
+ "Firing": "Активно",
+ "We could not load active alerts.": "Не удалось загрузить активные оповещения.",
+ "No active alerts.": "Активных оповещений нет.",
+ "This process is not currently reporting a sustained CPU, memory, or disk alert.": "Этот процесс сейчас не сообщает о длительном оповещении по CPU, памяти или диску."
}
}
diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json
index 7c4f0cb6..10f152b5 100644
--- a/web/default/src/i18n/locales/vi.json
+++ b/web/default/src/i18n/locales/vi.json
@@ -3407,6 +3407,7 @@
"Peak": "Đỉnh",
"Peak throughput": "Thông lượng đỉnh",
"Penalises repetition of frequent tokens": "Phạt việc lặp các token phổ biến",
+ "Paid - Needs Reconciliation": "Đã thanh toán - Cần đối soát",
"Pending": "Đang chờ",
"per": "per",
"Per 1K tokens": "Mỗi 1K tokens",
@@ -5354,6 +5355,110 @@
"Zero retention": "Không lưu dữ liệu",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",
- "Zoom": "Zoom"
+ "Zoom": "Zoom",
+ "Smart Operations": "Vận hành thông minh",
+ "Smart Operations Center": "Trung tâm vận hành thông minh",
+ "Production performance": "Hiệu năng thực tế",
+ "Production performance is not loaded automatically": "Dữ liệu hiệu năng thực tế không được tải tự động",
+ "Select Apply filters or Refresh to query the current time range.": "Chọn Áp dụng bộ lọc hoặc Làm mới để truy vấn khoảng thời gian hiện tại.",
+ "Read only": "Chỉ đọc",
+ "Legacy log": "Nhật ký cũ",
+ "Log fallback mode": "Chế độ dự phòng bằng nhật ký",
+ "This legacy view estimates outcomes from eligible Consume and Error logs. Known billing adjustments and channel probes are excluded, but it is not a complete Relay Attempt trace. Logged latency uses only non-retry samples and remains accurate only to whole seconds.": "Chế độ xem legacy này ước tính kết quả từ các nhật ký Consume và Error đủ điều kiện. Các điều chỉnh thanh toán đã biết và lượt kiểm tra kênh được loại trừ, nhưng đây không phải là dấu vết Relay Attempt đầy đủ. Độ trễ ghi nhật ký chỉ sử dụng các mẫu không thử lại và vẫn có độ chính xác đến giây nguyên.",
+ "Retry and latency data are temporarily unavailable": "Dữ liệu thử lại và độ trễ tạm thời không khả dụng",
+ "Historical retry markers are still being prepared. Request counts and estimated success rates remain available; retry counts and logged latency will appear after the backfill completes.": "Các dấu thử lại lịch sử vẫn đang được chuẩn bị. Số lượng yêu cầu và tỷ lệ thành công ước tính vẫn khả dụng; số lần thử lại và độ trễ ghi nhận sẽ xuất hiện sau khi hoàn tất điền bù.",
+ "The retry marker readiness state could not be verified. Request counts and estimated success rates remain available, but retry counts and logged latency are unavailable; check the application and log databases.": "Không thể xác minh trạng thái sẵn sàng của dấu thử lại. Số lượng yêu cầu và tỷ lệ thành công ước tính vẫn khả dụng, nhưng số lần thử lại và độ trễ ghi nhận chưa khả dụng; hãy kiểm tra cơ sở dữ liệu ứng dụng và nhật ký.",
+ "Time range": "Khoảng thời gian",
+ "All channels": "Tất cả kênh",
+ "All models": "Tất cả mô hình",
+ "Apply filters": "Áp dụng bộ lọc",
+ "Observed log events": "Sự kiện nhật ký quan sát được",
+ "Consume logs": "Nhật ký Consume",
+ "Consumed quota": "Hạn mức đã tiêu thụ",
+ "Estimated success rate": "Tỷ lệ thành công ước tính",
+ "Latency samples": "Mẫu độ trễ",
+ "Last observed": "Quan sát gần nhất",
+ "Unable to load production performance": "Không thể tải hiệu năng thực tế",
+ "Check log database availability and administrator permissions, then try again.": "Hãy kiểm tra cơ sở dữ liệu nhật ký và quyền quản trị rồi thử lại.",
+ "No recorded production logs": "Không có nhật ký thực tế đã ghi",
+ "Try a wider time range or check whether consumption and error logging are enabled.": "Hãy mở rộng khoảng thời gian hoặc kiểm tra nhật ký tiêu thụ và lỗi đã được bật chưa.",
+ "{{count}} rows": "{{count}} hàng",
+ "The table was truncated to {{count}} rows. Summary cards still cover all matching eligible logs.": "Bảng đã được cắt còn {{count}} hàng. Các thẻ tóm tắt vẫn bao phủ toàn bộ nhật ký đủ điều kiện khớp bộ lọc.",
+ "Generated at {{time}}": "Tạo lúc {{time}}",
+ "Production performance details": "Chi tiết hiệu năng thực tế",
+ "Channel type": "Loại kênh",
+ "Error logs": "Nhật ký lỗi",
+ "Logged latency": "Độ trễ theo nhật ký",
+ "Retries": "Số lần thử lại",
+ "Probe latency": "Độ trễ kiểm tra",
+ "Data quality": "Chất lượng dữ liệu",
+ "Interpret with caution": "Cần diễn giải thận trọng",
+ "Probe latency is shown for comparison only and is not included in production log latency or success-rate calculations.": "Độ trễ kiểm tra chỉ dùng để so sánh và không được tính vào độ trễ hoặc tỷ lệ thành công thực tế.",
+ "Partial data": "Dữ liệu một phần",
+ "Weak correlation": "Tương quan yếu",
+ "Heuristic outcome": "Kết quả suy đoán",
+ "Coarse latency": "Độ trễ thô",
+ "Probe data is separate": "Dữ liệu kiểm tra tách biệt",
+ "Known non-attempt logs excluded": "Đã loại trừ nhật ký không phải lần thử đã biết",
+ "Time range was limited": "Khoảng thời gian đã bị giới hạn",
+ "Results truncated": "Kết quả bị cắt bớt",
+ "Retry marker backfill pending": "Đang chờ điền bù dấu thử lại",
+ "Retry metrics unavailable": "Chỉ số thử lại không khả dụng",
+ "Consumption logs disabled": "Nhật ký tiêu thụ đã tắt",
+ "Error logs disabled": "Nhật ký lỗi đã tắt",
+ "Error log collection is disabled": "Tính năng thu thập nhật ký lỗi đang tắt",
+ "Errors may be incomplete": "Dữ liệu lỗi có thể chưa đầy đủ",
+ "Recorded error logs": "Nhật ký lỗi đã ghi nhận",
+ "The displayed error count includes only recorded logs and may be incomplete. Estimated success rate is unavailable while error log collection is disabled.": "Số lỗi hiển thị chỉ bao gồm các nhật ký đã ghi nhận và có thể chưa đầy đủ. Tỷ lệ thành công ước tính không khả dụng khi tính năng thu thập nhật ký lỗi đang tắt.",
+ "Channel metadata missing": "Thiếu siêu dữ liệu kênh",
+ "Probe unavailable": "Tính năng thăm dò không khả dụng",
+ "Channel performance": "Hiệu năng kênh",
+ "Channel performance details": "Chi tiết hiệu năng kênh",
+ "Channel performance is not loaded automatically": "Hiệu năng kênh không được tải tự động",
+ "No recorded channel logs": "Không có nhật ký kênh đã ghi",
+ "Unable to load channel performance": "Không thể tải hiệu năng kênh",
+ "Model performance": "Hiệu năng mô hình",
+ "Observed model log events": "Sự kiện nhật ký mô hình được quan sát",
+ "Unable to load model performance": "Không thể tải hiệu năng mô hình",
+ "Model performance is not loaded automatically": "Hiệu năng mô hình không được tải tự động",
+ "No recorded model logs": "Không có nhật ký mô hình đã ghi",
+ "Model performance details": "Chi tiết hiệu năng mô hình",
+ "This legacy view aggregates estimated model outcomes from eligible Consume and Error logs. It is not a complete Relay Attempt trace, and logged latency remains accurate only to whole seconds.": "Chế độ xem cũ này tổng hợp kết quả mô hình ước tính từ các nhật ký Consume và Error hợp lệ. Đây không phải là dấu vết Relay Attempt đầy đủ và độ trễ trong nhật ký chỉ chính xác đến giây.",
+ "Model results are aggregated from eligible logs and are not a complete Relay Attempt trace.": "Kết quả mô hình được tổng hợp từ các nhật ký hợp lệ và không phải là dấu vết Relay Attempt đầy đủ.",
+ "Inspect read-only production performance and operational evidence.": "Xem hiệu năng thực tế và bằng chứng vận hành ở chế độ chỉ đọc.",
+ "Throughput unavailable": "Không có thông lượng",
+ "Model throughput could not be read from performance metrics. Other log-based results remain available; check performance metrics collection and database availability.": "Không thể đọc thông lượng mô hình từ các chỉ số hiệu năng. Các kết quả dựa trên nhật ký khác vẫn khả dụng; hãy kiểm tra việc thu thập chỉ số hiệu năng và khả dụng của cơ sở dữ liệu.",
+ "Unable to load model performance details": "Không thể tải chi tiết hiệu năng mô hình",
+ "Check performance metrics collection and database availability, then try again.": "Hãy kiểm tra việc thu thập chỉ số hiệu năng và khả dụng của cơ sở dữ liệu, sau đó thử lại.",
+ "Bucket-level estimate": "Ước tính theo nhóm thời gian",
+ "New performance samples are not being collected. Existing performance data may be incomplete.": "Hệ thống không thu thập mẫu hiệu năng mới. Dữ liệu hiện có có thể chưa đầy đủ.",
+ "No performance samples were recorded for this time range.": "Không có mẫu hiệu năng nào được ghi nhận trong khoảng thời gian này.",
+ "No throughput samples": "Không có mẫu thông lượng",
+ "Performance metrics collection is disabled": "Đã tắt thu thập chỉ số hiệu năng",
+ "Performance metrics could not be queried. Log-based model results remain available; check the application database.": "Không thể truy vấn chỉ số hiệu năng. Kết quả mô hình dựa trên nhật ký vẫn khả dụng; hãy kiểm tra cơ sở dữ liệu ứng dụng.",
+ "Throughput coverage: {{start}} – {{end}} ({{minutes}} minute buckets).": "Phạm vi thông lượng: {{start}} – {{end}} (nhóm {{minutes}} phút).",
+ "Throughput coverage: {{start}} – {{end}}. Bucket granularity is unknown or mixed, so this range is approximate.": "Phạm vi thông lượng: {{start}} – {{end}}. Độ chi tiết của nhóm dữ liệu không xác định hoặc không đồng nhất, vì vậy phạm vi này chỉ là ước tính.",
+ "Throughput query failed": "Truy vấn thông lượng thất bại",
+ "Last 24 hours": "24 giờ qua",
+ "Channel models and groups from eligible production logs": "Mô hình và nhóm kênh từ nhật ký sản xuất hợp lệ",
+ "Unable to load channel performance details": "Không thể tải chi tiết hiệu năng kênh",
+ "No channel model performance": "Không có dữ liệu hiệu năng mô hình kênh",
+ "No eligible production logs were recorded for this channel in the last 24 hours.": "Không có nhật ký sản xuất hợp lệ nào được ghi cho kênh này trong 24 giờ qua.",
+ "The detail table was truncated to {{count}} rows. The summary still covers all matching eligible logs.": "Bảng chi tiết bị giới hạn ở {{count}} dòng. Phần tóm tắt vẫn bao phủ tất cả nhật ký hợp lệ phù hợp.",
+ "Throughput data is partial": "Dữ liệu thông lượng không đầy đủ",
+ "Stored and local performance samples are shown, but shared active buckets could not be read. Recent multi-node throughput may be incomplete.": "Các mẫu hiệu năng đã lưu và cục bộ được hiển thị, nhưng không thể đọc các bucket hoạt động dùng chung. Thông lượng đa nút gần đây có thể không đầy đủ.",
+ "Active Alerts": "Cảnh báo đang hoạt động",
+ "Alerts requiring administrator attention": "Cảnh báo cần quản trị viên chú ý",
+ "SmartOps reports sustained host pressure here for manual investigation and repair.": "SmartOps báo cáo tại đây tình trạng áp lực máy chủ kéo dài để quản trị viên kiểm tra và khắc phục thủ công.",
+ "Resource": "Tài nguyên",
+ "Threshold": "Ngưỡng",
+ "Observed": "Thời điểm quan sát",
+ "CPU usage": "Mức sử dụng CPU",
+ "Memory usage": "Mức sử dụng bộ nhớ",
+ "Disk usage": "Mức sử dụng đĩa",
+ "Firing": "Đang cảnh báo",
+ "We could not load active alerts.": "Không thể tải cảnh báo đang hoạt động.",
+ "No active alerts.": "Không có cảnh báo đang hoạt động.",
+ "This process is not currently reporting a sustained CPU, memory, or disk alert.": "Tiến trình này hiện không báo cáo cảnh báo CPU, bộ nhớ hoặc đĩa kéo dài."
}
}
diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json
index 8e623083..96fca30a 100644
--- a/web/default/src/i18n/locales/zh.json
+++ b/web/default/src/i18n/locales/zh.json
@@ -3407,6 +3407,7 @@
"Peak": "峰值",
"Peak throughput": "峰值吞吐",
"Penalises repetition of frequent tokens": "惩罚高频 token 的重复出现",
+ "Paid - Needs Reconciliation": "已付款,等待人工对账",
"Pending": "待确认",
"per": "每",
"Per 1K tokens": "每 1K tokens",
@@ -5354,6 +5355,110 @@
"Zero retention": "零数据保留",
"Zhipu": "智谱",
"Zhipu V4": "智谱 V4",
- "Zoom": "缩放"
+ "Zoom": "缩放",
+ "Smart Operations": "智能运维",
+ "Smart Operations Center": "智能运维中心",
+ "Production performance": "生产性能",
+ "Production performance is not loaded automatically": "生产性能不会自动加载",
+ "Select Apply filters or Refresh to query the current time range.": "请选择“应用筛选”或“刷新”,查询当前时间范围的数据。",
+ "Read only": "只读",
+ "Legacy log": "旧版日志",
+ "Log fallback mode": "日志回退模式",
+ "This legacy view estimates outcomes from eligible Consume and Error logs. Known billing adjustments and channel probes are excluded, but it is not a complete Relay Attempt trace. Logged latency uses only non-retry samples and remains accurate only to whole seconds.": "此日志回退视图根据符合条件的 Consume 与 Error 日志估算调用结果。已排除明确的账务调整和渠道探测,但它并非完整的 Relay Attempt 链路;日志延迟仅使用非重试样本,且精度仍仅到整秒。",
+ "Retry and latency data are temporarily unavailable": "重试和延迟数据暂不可用",
+ "Historical retry markers are still being prepared. Request counts and estimated success rates remain available; retry counts and logged latency will appear after the backfill completes.": "历史重试标记仍在准备中。请求计数和估算成功率仍可用;回填完成后将显示重试次数和日志延迟。",
+ "The retry marker readiness state could not be verified. Request counts and estimated success rates remain available, but retry counts and logged latency are unavailable; check the application and log databases.": "无法确认重试标记的就绪状态。请求计数和估算成功率仍可用,但重试次数和日志延迟暂不可用;请检查应用数据库和日志数据库。",
+ "Time range": "时间范围",
+ "All channels": "全部渠道",
+ "All models": "全部模型",
+ "Apply filters": "应用筛选",
+ "Observed log events": "观测日志事件",
+ "Consume logs": "消费日志",
+ "Consumed quota": "消耗额度",
+ "Estimated success rate": "估算成功率",
+ "Latency samples": "延迟样本",
+ "Last observed": "最近观测",
+ "Unable to load production performance": "无法加载生产性能",
+ "Check log database availability and administrator permissions, then try again.": "请检查日志数据库可用性和管理员权限后重试。",
+ "No recorded production logs": "没有已记录的生产日志",
+ "Try a wider time range or check whether consumption and error logging are enabled.": "请扩大时间范围,或检查消费日志与错误日志是否启用。",
+ "{{count}} rows": "{{count}} 行",
+ "The table was truncated to {{count}} rows. Summary cards still cover all matching eligible logs.": "表格已截断为 {{count}} 行,但摘要卡片仍覆盖所有符合条件的匹配日志。",
+ "Generated at {{time}}": "生成时间:{{time}}",
+ "Production performance details": "生产性能详情",
+ "Channel type": "渠道类型",
+ "Error logs": "错误日志",
+ "Logged latency": "日志延迟",
+ "Retries": "重试次数",
+ "Probe latency": "探测延迟",
+ "Data quality": "数据质量",
+ "Interpret with caution": "请谨慎解读",
+ "Probe latency is shown for comparison only and is not included in production log latency or success-rate calculations.": "探测延迟仅用于对比,不计入生产日志延迟或成功率计算。",
+ "Partial data": "部分数据",
+ "Weak correlation": "弱关联",
+ "Heuristic outcome": "启发式结果",
+ "Coarse latency": "粗粒度延迟",
+ "Probe data is separate": "探测数据独立展示",
+ "Known non-attempt logs excluded": "已排除已知非调用日志",
+ "Time range was limited": "时间范围已限制",
+ "Results truncated": "结果已截断",
+ "Retry marker backfill pending": "重试标记回填待完成",
+ "Retry metrics unavailable": "重试指标不可用",
+ "Consumption logs disabled": "消费日志未启用",
+ "Error logs disabled": "错误日志未启用",
+ "Error log collection is disabled": "错误日志采集未启用",
+ "Errors may be incomplete": "错误记录可能不完整",
+ "Recorded error logs": "已记录错误日志",
+ "The displayed error count includes only recorded logs and may be incomplete. Estimated success rate is unavailable while error log collection is disabled.": "当前显示的错误次数仅包含已记录日志,数据可能不完整;错误日志采集未启用期间无法计算估算成功率。",
+ "Channel metadata missing": "渠道元数据缺失",
+ "Probe unavailable": "探测不可用",
+ "Channel performance": "渠道性能",
+ "Channel performance details": "渠道性能详情",
+ "Channel performance is not loaded automatically": "渠道性能不会自动加载",
+ "No recorded channel logs": "没有已记录的渠道日志",
+ "Unable to load channel performance": "无法加载渠道性能",
+ "Model performance": "模型性能",
+ "Observed model log events": "观测模型日志事件",
+ "Unable to load model performance": "无法加载模型性能",
+ "Model performance is not loaded automatically": "模型性能不会自动加载",
+ "No recorded model logs": "没有已记录的模型日志",
+ "Model performance details": "模型性能详情",
+ "This legacy view aggregates estimated model outcomes from eligible Consume and Error logs. It is not a complete Relay Attempt trace, and logged latency remains accurate only to whole seconds.": "此旧版视图根据符合条件的 Consume 和 Error 日志汇总估算模型结果。它不是完整的 Relay Attempt 链路,日志延迟仅精确到整秒。",
+ "Model results are aggregated from eligible logs and are not a complete Relay Attempt trace.": "模型结果根据符合条件的日志汇总,并不是完整的 Relay Attempt 链路。",
+ "Inspect read-only production performance and operational evidence.": "查看只读生产性能和运维证据。",
+ "Throughput unavailable": "吞吐量不可用",
+ "Model throughput could not be read from performance metrics. Other log-based results remain available; check performance metrics collection and database availability.": "无法从性能指标中读取模型吞吐量。其他基于日志的结果仍可用;请检查性能指标采集和数据库可用性。",
+ "Unable to load model performance details": "无法加载模型性能详情",
+ "Check performance metrics collection and database availability, then try again.": "请检查性能指标采集和数据库可用性,然后重试。",
+ "Bucket-level estimate": "桶级估算",
+ "New performance samples are not being collected. Existing performance data may be incomplete.": "当前未采集新的性能样本。现有性能数据可能不完整。",
+ "No performance samples were recorded for this time range.": "该时间范围内未记录性能样本。",
+ "No throughput samples": "无吞吐量样本",
+ "Performance metrics collection is disabled": "性能指标采集已关闭",
+ "Performance metrics could not be queried. Log-based model results remain available; check the application database.": "无法查询性能指标。基于日志的模型结果仍可用;请检查应用数据库。",
+ "Throughput coverage: {{start}} – {{end}} ({{minutes}} minute buckets).": "吞吐量覆盖范围:{{start}} – {{end}}({{minutes}} 分钟桶)。",
+ "Throughput coverage: {{start}} – {{end}}. Bucket granularity is unknown or mixed, so this range is approximate.": "吞吐量覆盖范围:{{start}} – {{end}}。桶粒度未知或混合,因此该范围为近似值。",
+ "Throughput query failed": "吞吐量查询失败",
+ "Last 24 hours": "最近 24 小时",
+ "Channel models and groups from eligible production logs": "来自符合条件生产日志的渠道模型和分组",
+ "Unable to load channel performance details": "无法加载渠道性能详情",
+ "No channel model performance": "没有渠道模型性能数据",
+ "No eligible production logs were recorded for this channel in the last 24 hours.": "最近 24 小时未记录该渠道的符合条件生产日志。",
+ "The detail table was truncated to {{count}} rows. The summary still covers all matching eligible logs.": "详情表已截断为 {{count}} 行,但摘要仍覆盖所有符合条件的匹配日志。",
+ "Throughput data is partial": "吞吐量数据不完整",
+ "Stored and local performance samples are shown, but shared active buckets could not be read. Recent multi-node throughput may be incomplete.": "当前显示已存储和本地性能样本,但无法读取共享活动桶;最近的多节点吞吐量可能不完整。",
+ "Active Alerts": "活动告警",
+ "Alerts requiring administrator attention": "需要管理员关注的告警",
+ "SmartOps reports sustained host pressure here for manual investigation and repair.": "SmartOps 会在此报告持续的主机资源压力,供管理员人工排查和修复。",
+ "Resource": "资源",
+ "Threshold": "阈值",
+ "Observed": "观测时间",
+ "CPU usage": "CPU 使用率",
+ "Memory usage": "内存使用率",
+ "Disk usage": "磁盘使用率",
+ "Firing": "告警中",
+ "We could not load active alerts.": "无法加载活动告警。",
+ "No active alerts.": "当前没有活动告警。",
+ "This process is not currently reporting a sustained CPU, memory, or disk alert.": "当前进程暂未报告持续的 CPU、内存或磁盘告警。"
}
}
diff --git a/web/default/src/i18n/static-keys.ts b/web/default/src/i18n/static-keys.ts
index 5c4f3989..bd254b57 100644
--- a/web/default/src/i18n/static-keys.ts
+++ b/web/default/src/i18n/static-keys.ts
@@ -83,6 +83,9 @@ export const STATIC_I18N_KEYS = [
'User',
'Guest',
+ // Billing statuses
+ 'Paid - Needs Reconciliation',
+
// API Keys
'Enabled',
'Disabled',
diff --git a/web/default/src/lib/oauth-binding-contract.test.ts b/web/default/src/lib/oauth-binding-contract.test.ts
new file mode 100644
index 00000000..60b55279
--- /dev/null
+++ b/web/default/src/lib/oauth-binding-contract.test.ts
@@ -0,0 +1,39 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+import {
+ adminCustomOAuthUnbindPath,
+ indexCustomOAuthBindings,
+ selfCustomOAuthUnbindPath,
+ type CustomOAuthBinding,
+} from './oauth'
+
+describe('custom OAuth binding contract', () => {
+ test('indexes numeric provider IDs and exposes provider_user_id', () => {
+ const binding: CustomOAuthBinding = {
+ provider_id: 7,
+ provider_name: 'Company SSO',
+ provider_slug: 'company-sso',
+ provider_icon: '',
+ provider_user_id: 'external-42',
+ }
+ const indexed = indexCustomOAuthBindings([binding])
+ assert.equal(indexed.get(7)?.provider_user_id, 'external-42')
+ assert.equal(indexed.get(Number('7'))?.provider_name, 'Company SSO')
+ })
+
+ test('builds user and admin unbind endpoints from numeric IDs', () => {
+ assert.equal(selfCustomOAuthUnbindPath(7), '/api/user/oauth/bindings/7')
+ assert.equal(
+ adminCustomOAuthUnbindPath(42, 7),
+ '/api/user/42/oauth/bindings/7'
+ )
+ })
+})
diff --git a/web/default/src/lib/oauth.ts b/web/default/src/lib/oauth.ts
index a43c1e4e..31daea93 100644
--- a/web/default/src/lib/oauth.ts
+++ b/web/default/src/lib/oauth.ts
@@ -18,6 +18,31 @@ For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API
*/
import { api } from './api'
+export interface CustomOAuthBinding {
+ provider_id: number
+ provider_name: string
+ provider_slug: string
+ provider_icon: string
+ provider_user_id: string
+}
+
+export function indexCustomOAuthBindings(
+ bindings: CustomOAuthBinding[]
+): Map {
+ return new Map(bindings.map((binding) => [binding.provider_id, binding]))
+}
+
+export function selfCustomOAuthUnbindPath(providerId: number): string {
+ return `/api/user/oauth/bindings/${providerId}`
+}
+
+export function adminCustomOAuthUnbindPath(
+ userId: number,
+ providerId: number
+): string {
+ return `/api/user/${userId}/oauth/bindings/${providerId}`
+}
+
const oauthBindingStatePrefix = 'oauth:binding:state:'
const oauthBindingStateTtlMs = 10 * 60 * 1000
diff --git a/web/default/src/routeTree.gen.ts b/web/default/src/routeTree.gen.ts
index e0add2a9..5092b979 100644
--- a/web/default/src/routeTree.gen.ts
+++ b/web/default/src/routeTree.gen.ts
@@ -50,6 +50,11 @@ import { Route as AuthenticatedKeysIndexRouteImport } from './routes/_authentica
import { Route as AuthenticatedDashboardIndexRouteImport } from './routes/_authenticated/dashboard/index'
import { Route as AuthenticatedChannelsIndexRouteImport } from './routes/_authenticated/channels/index'
import { Route as AuthenticatedUsageLogsSectionRouteImport } from './routes/_authenticated/usage-logs/$section'
+import { Route as AuthenticatedSmartOpsSystemInfoRouteImport } from './routes/_authenticated/smart-ops/system-info'
+import { Route as AuthenticatedSmartOpsProductionPerformanceRouteImport } from './routes/_authenticated/smart-ops/production-performance'
+import { Route as AuthenticatedSmartOpsModelPerformanceRouteImport } from './routes/_authenticated/smart-ops/model-performance'
+import { Route as AuthenticatedSmartOpsChannelPerformanceRouteImport } from './routes/_authenticated/smart-ops/channel-performance'
+import { Route as AuthenticatedSmartOpsAlertsRouteImport } from './routes/_authenticated/smart-ops/alerts'
import { Route as AuthenticatedModelsSectionRouteImport } from './routes/_authenticated/models/$section'
import { Route as AuthenticatedErrorsErrorRouteImport } from './routes/_authenticated/errors/$error'
import { Route as AuthenticatedDashboardSectionRouteImport } from './routes/_authenticated/dashboard/$section'
@@ -286,6 +291,36 @@ const AuthenticatedUsageLogsSectionRoute =
path: '/usage-logs/$section',
getParentRoute: () => AuthenticatedRouteRoute,
} as any)
+const AuthenticatedSmartOpsSystemInfoRoute =
+ AuthenticatedSmartOpsSystemInfoRouteImport.update({
+ id: '/smart-ops/system-info',
+ path: '/smart-ops/system-info',
+ getParentRoute: () => AuthenticatedRouteRoute,
+ } as any)
+const AuthenticatedSmartOpsProductionPerformanceRoute =
+ AuthenticatedSmartOpsProductionPerformanceRouteImport.update({
+ id: '/smart-ops/production-performance',
+ path: '/smart-ops/production-performance',
+ getParentRoute: () => AuthenticatedRouteRoute,
+ } as any)
+const AuthenticatedSmartOpsModelPerformanceRoute =
+ AuthenticatedSmartOpsModelPerformanceRouteImport.update({
+ id: '/smart-ops/model-performance',
+ path: '/smart-ops/model-performance',
+ getParentRoute: () => AuthenticatedRouteRoute,
+ } as any)
+const AuthenticatedSmartOpsChannelPerformanceRoute =
+ AuthenticatedSmartOpsChannelPerformanceRouteImport.update({
+ id: '/smart-ops/channel-performance',
+ path: '/smart-ops/channel-performance',
+ getParentRoute: () => AuthenticatedRouteRoute,
+ } as any)
+const AuthenticatedSmartOpsAlertsRoute =
+ AuthenticatedSmartOpsAlertsRouteImport.update({
+ id: '/smart-ops/alerts',
+ path: '/smart-ops/alerts',
+ getParentRoute: () => AuthenticatedRouteRoute,
+ } as any)
const AuthenticatedModelsSectionRoute =
AuthenticatedModelsSectionRouteImport.update({
id: '/models/$section',
@@ -429,6 +464,11 @@ export interface FileRoutesByFullPath {
'/dashboard/$section': typeof AuthenticatedDashboardSectionRoute
'/errors/$error': typeof AuthenticatedErrorsErrorRoute
'/models/$section': typeof AuthenticatedModelsSectionRoute
+ '/smart-ops/alerts': typeof AuthenticatedSmartOpsAlertsRoute
+ '/smart-ops/channel-performance': typeof AuthenticatedSmartOpsChannelPerformanceRoute
+ '/smart-ops/model-performance': typeof AuthenticatedSmartOpsModelPerformanceRoute
+ '/smart-ops/production-performance': typeof AuthenticatedSmartOpsProductionPerformanceRoute
+ '/smart-ops/system-info': typeof AuthenticatedSmartOpsSystemInfoRoute
'/usage-logs/$section': typeof AuthenticatedUsageLogsSectionRoute
'/channels/': typeof AuthenticatedChannelsIndexRoute
'/dashboard/': typeof AuthenticatedDashboardIndexRoute
@@ -488,6 +528,11 @@ export interface FileRoutesByTo {
'/dashboard/$section': typeof AuthenticatedDashboardSectionRoute
'/errors/$error': typeof AuthenticatedErrorsErrorRoute
'/models/$section': typeof AuthenticatedModelsSectionRoute
+ '/smart-ops/alerts': typeof AuthenticatedSmartOpsAlertsRoute
+ '/smart-ops/channel-performance': typeof AuthenticatedSmartOpsChannelPerformanceRoute
+ '/smart-ops/model-performance': typeof AuthenticatedSmartOpsModelPerformanceRoute
+ '/smart-ops/production-performance': typeof AuthenticatedSmartOpsProductionPerformanceRoute
+ '/smart-ops/system-info': typeof AuthenticatedSmartOpsSystemInfoRoute
'/usage-logs/$section': typeof AuthenticatedUsageLogsSectionRoute
'/channels': typeof AuthenticatedChannelsIndexRoute
'/dashboard': typeof AuthenticatedDashboardIndexRoute
@@ -551,6 +596,11 @@ export interface FileRoutesById {
'/_authenticated/dashboard/$section': typeof AuthenticatedDashboardSectionRoute
'/_authenticated/errors/$error': typeof AuthenticatedErrorsErrorRoute
'/_authenticated/models/$section': typeof AuthenticatedModelsSectionRoute
+ '/_authenticated/smart-ops/alerts': typeof AuthenticatedSmartOpsAlertsRoute
+ '/_authenticated/smart-ops/channel-performance': typeof AuthenticatedSmartOpsChannelPerformanceRoute
+ '/_authenticated/smart-ops/model-performance': typeof AuthenticatedSmartOpsModelPerformanceRoute
+ '/_authenticated/smart-ops/production-performance': typeof AuthenticatedSmartOpsProductionPerformanceRoute
+ '/_authenticated/smart-ops/system-info': typeof AuthenticatedSmartOpsSystemInfoRoute
'/_authenticated/usage-logs/$section': typeof AuthenticatedUsageLogsSectionRoute
'/_authenticated/channels/': typeof AuthenticatedChannelsIndexRoute
'/_authenticated/dashboard/': typeof AuthenticatedDashboardIndexRoute
@@ -613,6 +663,11 @@ export interface FileRouteTypes {
| '/dashboard/$section'
| '/errors/$error'
| '/models/$section'
+ | '/smart-ops/alerts'
+ | '/smart-ops/channel-performance'
+ | '/smart-ops/model-performance'
+ | '/smart-ops/production-performance'
+ | '/smart-ops/system-info'
| '/usage-logs/$section'
| '/channels/'
| '/dashboard/'
@@ -672,6 +727,11 @@ export interface FileRouteTypes {
| '/dashboard/$section'
| '/errors/$error'
| '/models/$section'
+ | '/smart-ops/alerts'
+ | '/smart-ops/channel-performance'
+ | '/smart-ops/model-performance'
+ | '/smart-ops/production-performance'
+ | '/smart-ops/system-info'
| '/usage-logs/$section'
| '/channels'
| '/dashboard'
@@ -734,6 +794,11 @@ export interface FileRouteTypes {
| '/_authenticated/dashboard/$section'
| '/_authenticated/errors/$error'
| '/_authenticated/models/$section'
+ | '/_authenticated/smart-ops/alerts'
+ | '/_authenticated/smart-ops/channel-performance'
+ | '/_authenticated/smart-ops/model-performance'
+ | '/_authenticated/smart-ops/production-performance'
+ | '/_authenticated/smart-ops/system-info'
| '/_authenticated/usage-logs/$section'
| '/_authenticated/channels/'
| '/_authenticated/dashboard/'
@@ -1075,6 +1140,41 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedUsageLogsSectionRouteImport
parentRoute: typeof AuthenticatedRouteRoute
}
+ '/_authenticated/smart-ops/system-info': {
+ id: '/_authenticated/smart-ops/system-info'
+ path: '/smart-ops/system-info'
+ fullPath: '/smart-ops/system-info'
+ preLoaderRoute: typeof AuthenticatedSmartOpsSystemInfoRouteImport
+ parentRoute: typeof AuthenticatedRouteRoute
+ }
+ '/_authenticated/smart-ops/production-performance': {
+ id: '/_authenticated/smart-ops/production-performance'
+ path: '/smart-ops/production-performance'
+ fullPath: '/smart-ops/production-performance'
+ preLoaderRoute: typeof AuthenticatedSmartOpsProductionPerformanceRouteImport
+ parentRoute: typeof AuthenticatedRouteRoute
+ }
+ '/_authenticated/smart-ops/model-performance': {
+ id: '/_authenticated/smart-ops/model-performance'
+ path: '/smart-ops/model-performance'
+ fullPath: '/smart-ops/model-performance'
+ preLoaderRoute: typeof AuthenticatedSmartOpsModelPerformanceRouteImport
+ parentRoute: typeof AuthenticatedRouteRoute
+ }
+ '/_authenticated/smart-ops/channel-performance': {
+ id: '/_authenticated/smart-ops/channel-performance'
+ path: '/smart-ops/channel-performance'
+ fullPath: '/smart-ops/channel-performance'
+ preLoaderRoute: typeof AuthenticatedSmartOpsChannelPerformanceRouteImport
+ parentRoute: typeof AuthenticatedRouteRoute
+ }
+ '/_authenticated/smart-ops/alerts': {
+ id: '/_authenticated/smart-ops/alerts'
+ path: '/smart-ops/alerts'
+ fullPath: '/smart-ops/alerts'
+ preLoaderRoute: typeof AuthenticatedSmartOpsAlertsRouteImport
+ parentRoute: typeof AuthenticatedRouteRoute
+ }
'/_authenticated/models/$section': {
id: '/_authenticated/models/$section'
path: '/models/$section'
@@ -1301,6 +1401,11 @@ interface AuthenticatedRouteRouteChildren {
AuthenticatedDashboardSectionRoute: typeof AuthenticatedDashboardSectionRoute
AuthenticatedErrorsErrorRoute: typeof AuthenticatedErrorsErrorRoute
AuthenticatedModelsSectionRoute: typeof AuthenticatedModelsSectionRoute
+ AuthenticatedSmartOpsAlertsRoute: typeof AuthenticatedSmartOpsAlertsRoute
+ AuthenticatedSmartOpsChannelPerformanceRoute: typeof AuthenticatedSmartOpsChannelPerformanceRoute
+ AuthenticatedSmartOpsModelPerformanceRoute: typeof AuthenticatedSmartOpsModelPerformanceRoute
+ AuthenticatedSmartOpsProductionPerformanceRoute: typeof AuthenticatedSmartOpsProductionPerformanceRoute
+ AuthenticatedSmartOpsSystemInfoRoute: typeof AuthenticatedSmartOpsSystemInfoRoute
AuthenticatedUsageLogsSectionRoute: typeof AuthenticatedUsageLogsSectionRoute
AuthenticatedChannelsIndexRoute: typeof AuthenticatedChannelsIndexRoute
AuthenticatedDashboardIndexRoute: typeof AuthenticatedDashboardIndexRoute
@@ -1324,6 +1429,14 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
AuthenticatedDashboardSectionRoute: AuthenticatedDashboardSectionRoute,
AuthenticatedErrorsErrorRoute: AuthenticatedErrorsErrorRoute,
AuthenticatedModelsSectionRoute: AuthenticatedModelsSectionRoute,
+ AuthenticatedSmartOpsAlertsRoute: AuthenticatedSmartOpsAlertsRoute,
+ AuthenticatedSmartOpsChannelPerformanceRoute:
+ AuthenticatedSmartOpsChannelPerformanceRoute,
+ AuthenticatedSmartOpsModelPerformanceRoute:
+ AuthenticatedSmartOpsModelPerformanceRoute,
+ AuthenticatedSmartOpsProductionPerformanceRoute:
+ AuthenticatedSmartOpsProductionPerformanceRoute,
+ AuthenticatedSmartOpsSystemInfoRoute: AuthenticatedSmartOpsSystemInfoRoute,
AuthenticatedUsageLogsSectionRoute: AuthenticatedUsageLogsSectionRoute,
AuthenticatedChannelsIndexRoute: AuthenticatedChannelsIndexRoute,
AuthenticatedDashboardIndexRoute: AuthenticatedDashboardIndexRoute,
diff --git a/web/default/src/routes/_authenticated/smart-ops/alerts.tsx b/web/default/src/routes/_authenticated/smart-ops/alerts.tsx
new file mode 100644
index 00000000..4e6481f3
--- /dev/null
+++ b/web/default/src/routes/_authenticated/smart-ops/alerts.tsx
@@ -0,0 +1,32 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { useAuthStore } from '@/stores/auth-store'
+import { ROLE } from '@/lib/roles'
+import { ActiveAlerts } from '@/features/smart-ops/alerts'
+
+export const Route = createFileRoute('/_authenticated/smart-ops/alerts')({
+ beforeLoad: () => {
+ const { auth } = useAuthStore.getState()
+ if (!auth.user || auth.user.role < ROLE.ADMIN) {
+ throw redirect({ to: '/403' })
+ }
+ },
+ component: ActiveAlerts,
+})
diff --git a/web/default/src/routes/_authenticated/smart-ops/channel-performance.tsx b/web/default/src/routes/_authenticated/smart-ops/channel-performance.tsx
new file mode 100644
index 00000000..9204438d
--- /dev/null
+++ b/web/default/src/routes/_authenticated/smart-ops/channel-performance.tsx
@@ -0,0 +1,34 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { useAuthStore } from '@/stores/auth-store'
+import { ROLE } from '@/lib/roles'
+import { ProductionPerformance } from '@/features/smart-ops'
+
+export const Route = createFileRoute(
+ '/_authenticated/smart-ops/channel-performance'
+)({
+ beforeLoad: () => {
+ const { auth } = useAuthStore.getState()
+ if (!auth.user || auth.user.role < ROLE.ADMIN) {
+ throw redirect({ to: '/403' })
+ }
+ },
+ component: ProductionPerformance,
+})
diff --git a/web/default/src/routes/_authenticated/smart-ops/model-performance.tsx b/web/default/src/routes/_authenticated/smart-ops/model-performance.tsx
new file mode 100644
index 00000000..b0313947
--- /dev/null
+++ b/web/default/src/routes/_authenticated/smart-ops/model-performance.tsx
@@ -0,0 +1,34 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { useAuthStore } from '@/stores/auth-store'
+import { ROLE } from '@/lib/roles'
+import { ModelPerformance } from '@/features/smart-ops/model-performance'
+
+export const Route = createFileRoute(
+ '/_authenticated/smart-ops/model-performance'
+)({
+ beforeLoad: () => {
+ const { auth } = useAuthStore.getState()
+ if (!auth.user || auth.user.role < ROLE.ADMIN) {
+ throw redirect({ to: '/403' })
+ }
+ },
+ component: ModelPerformance,
+})
diff --git a/web/default/src/routes/_authenticated/smart-ops/production-performance.tsx b/web/default/src/routes/_authenticated/smart-ops/production-performance.tsx
new file mode 100644
index 00000000..526304b9
--- /dev/null
+++ b/web/default/src/routes/_authenticated/smart-ops/production-performance.tsx
@@ -0,0 +1,27 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute(
+ '/_authenticated/smart-ops/production-performance'
+)({
+ beforeLoad: () => {
+ throw redirect({ to: '/smart-ops/channel-performance' })
+ },
+})
diff --git a/web/default/src/routes/_authenticated/smart-ops/system-info.tsx b/web/default/src/routes/_authenticated/smart-ops/system-info.tsx
new file mode 100644
index 00000000..3401eafb
--- /dev/null
+++ b/web/default/src/routes/_authenticated/smart-ops/system-info.tsx
@@ -0,0 +1,32 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { useAuthStore } from '@/stores/auth-store'
+import { ROLE } from '@/lib/roles'
+import { SystemInfo } from '@/features/system-info'
+
+export const Route = createFileRoute('/_authenticated/smart-ops/system-info')({
+ beforeLoad: () => {
+ const { auth } = useAuthStore.getState()
+ if (auth.user?.role !== ROLE.SUPER_ADMIN) {
+ throw redirect({ to: '/403' })
+ }
+ },
+ component: SystemInfo,
+})
diff --git a/web/default/src/routes/_authenticated/system-info/index.tsx b/web/default/src/routes/_authenticated/system-info/index.tsx
index be79abac..5c512153 100644
--- a/web/default/src/routes/_authenticated/system-info/index.tsx
+++ b/web/default/src/routes/_authenticated/system-info/index.tsx
@@ -19,7 +19,6 @@ For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API
import { createFileRoute, redirect } from '@tanstack/react-router'
import { useAuthStore } from '@/stores/auth-store'
import { ROLE } from '@/lib/roles'
-import { SystemInfo } from '@/features/system-info'
export const Route = createFileRoute('/_authenticated/system-info/')({
beforeLoad: () => {
@@ -30,6 +29,7 @@ export const Route = createFileRoute('/_authenticated/system-info/')({
to: '/403',
})
}
+
+ throw redirect({ to: '/smart-ops/system-info' })
},
- component: SystemInfo,
})
diff --git a/web/default/src/test/react.test.ts b/web/default/src/test/react.test.ts
new file mode 100644
index 00000000..00cb4560
--- /dev/null
+++ b/web/default/src/test/react.test.ts
@@ -0,0 +1,101 @@
+/*
+Copyright (C) 2023-2026 MAX-API-Next
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
+*/
+import assert from 'node:assert/strict'
+import { test } from 'node:test'
+import { createReactTestEnvironment } from './react'
+
+test('teardown cancels pending animation frames before removing browser globals', async (): Promise => {
+ const testEnv = createReactTestEnvironment()
+ await testEnv.setup()
+ let callbackRan = false
+
+ requestAnimationFrame(() => {
+ callbackRan = true
+ void window.document
+ })
+
+ testEnv.teardown()
+ await new Promise((resolve) => setTimeout(resolve, 10))
+
+ assert.equal(callbackRan, false)
+})
+
+test('cancelAnimationFrame cancels a frame owned by the current environment', async (): Promise => {
+ const testEnv = createReactTestEnvironment()
+ await testEnv.setup()
+ let callbackRan = false
+
+ try {
+ const handle = requestAnimationFrame(() => {
+ callbackRan = true
+ })
+ cancelAnimationFrame(handle)
+
+ await new Promise((resolve) => setTimeout(resolve, 10))
+ assert.equal(callbackRan, false)
+ } finally {
+ testEnv.teardown()
+ }
+})
+
+test('a captured animation frame scheduler cannot leak across test environments', async (): Promise => {
+ const firstEnv = createReactTestEnvironment()
+ await firstEnv.setup()
+ const capturedRequestAnimationFrame = requestAnimationFrame
+ firstEnv.teardown()
+
+ const secondEnv = createReactTestEnvironment()
+ await secondEnv.setup()
+ let callbackRan = false
+
+ try {
+ capturedRequestAnimationFrame(() => {
+ callbackRan = true
+ void window.document
+ })
+
+ await new Promise((resolve) => setTimeout(resolve, 10))
+ assert.equal(callbackRan, false)
+ } finally {
+ secondEnv.teardown()
+ }
+})
+
+test('a captured animation frame canceller cannot cancel a later environment', async (): Promise => {
+ const firstEnv = createReactTestEnvironment()
+ await firstEnv.setup()
+ const capturedCancelAnimationFrame = cancelAnimationFrame
+ firstEnv.teardown()
+
+ const secondEnv = createReactTestEnvironment()
+ await secondEnv.setup()
+ let callbackRan = false
+
+ try {
+ const handle = requestAnimationFrame(() => {
+ callbackRan = true
+ })
+ capturedCancelAnimationFrame(handle)
+
+ await new Promise((resolve) => setTimeout(resolve, 10))
+ assert.equal(callbackRan, true)
+ } finally {
+ secondEnv.teardown()
+ }
+})
diff --git a/web/default/src/test/react.tsx b/web/default/src/test/react.tsx
index f2b4ccd1..ed63e8fc 100644
--- a/web/default/src/test/react.tsx
+++ b/web/default/src/test/react.tsx
@@ -48,6 +48,45 @@ class ResizeObserverStub {
disconnect() {}
}
+type AnimationFrameTimer = ReturnType
+type TestWindow = typeof globalThis.window
+
+const animationFrameHandles = new Map()
+
+function createTestAnimationFrameScheduler(
+ window: TestWindow
+): (callback: FrameRequestCallback) => number {
+ return (callback: FrameRequestCallback): number => {
+ if (globalThis.window !== window) return 0
+ const handle = setTimeout(() => {
+ animationFrameHandles.delete(handle)
+ if (globalThis.window !== window) return
+ callback(Date.now())
+ }, 0)
+ animationFrameHandles.set(handle, window)
+ return handle as unknown as number
+ }
+}
+
+function createTestAnimationFrameCanceller(
+ window: TestWindow
+): (handle: number) => void {
+ return (handle: number): void => {
+ const timerHandle = handle as unknown as AnimationFrameTimer
+ if (animationFrameHandles.get(timerHandle) !== window) return
+ clearTimeout(timerHandle)
+ animationFrameHandles.delete(timerHandle)
+ }
+}
+
+function cancelPendingAnimationFrames(window: TestWindow) {
+ for (const [handle, scheduledWindow] of animationFrameHandles) {
+ if (scheduledWindow !== window) continue
+ clearTimeout(handle)
+ animationFrameHandles.delete(handle)
+ }
+}
+
export function createReactTestEnvironment(options?: {
lng?: string
fallbackLng?: string
@@ -64,6 +103,7 @@ export function createReactTestEnvironment(options?: {
GlobalKey,
PropertyDescriptor | undefined
>()
+ let installedWindow: TestWindow | undefined
function setGlobal(key: GlobalKey, value: unknown) {
previousDescriptors.set(
@@ -102,6 +142,7 @@ export function createReactTestEnvironment(options?: {
})
const window = dom.window
+ installedWindow = window as unknown as TestWindow
Object.defineProperty(window, 'matchMedia', {
configurable: true,
value: () => ({
@@ -127,16 +168,21 @@ export function createReactTestEnvironment(options?: {
setGlobal('KeyboardEvent', window.KeyboardEvent)
setGlobal('MutationObserver', window.MutationObserver)
setGlobal('getComputedStyle', window.getComputedStyle.bind(window))
- setGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
- setTimeout(() => callback(Date.now()), 0)
+ setGlobal(
+ 'requestAnimationFrame',
+ createTestAnimationFrameScheduler(installedWindow)
)
- setGlobal('cancelAnimationFrame', (handle: number) =>
- clearTimeout(handle)
+ setGlobal(
+ 'cancelAnimationFrame',
+ createTestAnimationFrameCanceller(installedWindow)
)
setGlobal('ResizeObserver', ResizeObserverStub)
setGlobal('IS_REACT_ACT_ENVIRONMENT', true)
},
teardown() {
+ if (installedWindow) {
+ cancelPendingAnimationFrames(installedWindow)
+ }
restoreGlobals()
dom.window.close()
},