diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/accountQuota.ts b/cliproxyapi-pro-management/overlay/src/features/monitoring/accountQuota.ts new file mode 100644 index 0000000..4b01fb6 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/accountQuota.ts @@ -0,0 +1,209 @@ +import type { ReactNode } from 'react'; +import type { TFunction } from 'i18next'; +import { + ANTIGRAVITY_CONFIG, + CLAUDE_CONFIG, + CODEX_CONFIG, + KIMI_CONFIG, + XAI_CONFIG, + type QuotaConfig, + type QuotaStore, +} from '@/components/quota/quotaConfigs'; +import type { QuotaRenderHelpers, QuotaStatusState } from '@/components/quota/QuotaCard'; +import type { MonitoringEventRow } from './hooks/useMonitoringData'; +import type { AuthFileItem } from '@/types'; +import { getStatusFromError, isAntigravityFile, isClaudeFile, isCodexFile, isKimiFile, isXaiFile } from '@/utils/quota'; +import { normalizeAuthIndex } from '@/utils/usage'; + +export type AnyQuotaConfig = { + type: QuotaConfig['type']; + i18nPrefix: string; + fetchQuota: (file: AuthFileItem, t: TFunction) => Promise; + storeSelector: (state: QuotaStore) => Record; + storeSetter: keyof QuotaStore; + buildLoadingState: () => QuotaStatusState; + buildSuccessState: (data: unknown) => QuotaStatusState; + buildErrorState: (message: string, status?: number) => QuotaStatusState; + renderQuotaItems: (quota: QuotaStatusState, t: TFunction, helpers: QuotaRenderHelpers) => ReactNode; +}; + +const adaptQuotaConfig = ( + config: QuotaConfig +): AnyQuotaConfig => ({ + type: config.type, + i18nPrefix: config.i18nPrefix, + fetchQuota: config.fetchQuota, + storeSelector: config.storeSelector, + storeSetter: config.storeSetter, + buildLoadingState: config.buildLoadingState, + buildSuccessState: (data) => config.buildSuccessState(data as TData), + buildErrorState: config.buildErrorState, + renderQuotaItems: (quota, t, helpers) => config.renderQuotaItems(quota as TState, t, helpers), +}); + +const ACCOUNT_ANTIGRAVITY_QUOTA_CONFIG = adaptQuotaConfig(ANTIGRAVITY_CONFIG); +const ACCOUNT_CLAUDE_QUOTA_CONFIG = adaptQuotaConfig(CLAUDE_CONFIG); +const ACCOUNT_CODEX_QUOTA_CONFIG = adaptQuotaConfig(CODEX_CONFIG); +const ACCOUNT_KIMI_QUOTA_CONFIG = adaptQuotaConfig(KIMI_CONFIG); +const ACCOUNT_XAI_QUOTA_CONFIG = adaptQuotaConfig(XAI_CONFIG); + +export type AccountQuotaTarget = { + key: string; + authIndex: string; + authLabel: string; + fileName: string; + file: AuthFileItem; + config: AnyQuotaConfig; +}; + +export type AccountQuotaEntry = { + key: string; + authLabel: string; + fileName: string; + providerLabel: string; + quota?: QuotaStatusState; + config: AnyQuotaConfig; +}; + +export type AccountQuotaState = { + status: 'idle' | 'loading' | 'success' | 'error'; + targetKey: string; + error?: string; + lastRefreshedAt?: number; +}; + +export type AccountQuotaSourceRow = Pick; + +export const getQuotaProviderLabel = (config: AnyQuotaConfig, t: TFunction) => { + const titleKey = `${config.i18nPrefix}.title`; + const translated = t(titleKey); + if (translated !== titleKey) return translated; + return config.type; +}; + +export const getAccountQuotaConfig = (file: AuthFileItem): AnyQuotaConfig | undefined => { + if (isAntigravityFile(file)) return ACCOUNT_ANTIGRAVITY_QUOTA_CONFIG; + if (isClaudeFile(file)) return ACCOUNT_CLAUDE_QUOTA_CONFIG; + if (isCodexFile(file)) return ACCOUNT_CODEX_QUOTA_CONFIG; + if (isKimiFile(file)) return ACCOUNT_KIMI_QUOTA_CONFIG; + if (isXaiFile(file)) return ACCOUNT_XAI_QUOTA_CONFIG; + return undefined; +}; + +export const resolveQuotaErrorMessage = (t: TFunction, quota?: QuotaStatusState): string => { + if (!quota) return t('common.unknown_error'); + if (quota.errorStatus === 404) return t('common.quota_update_required'); + if (quota.errorStatus === 403) return t('common.quota_check_credential'); + return quota.error || t('common.unknown_error'); +}; + +export const hasUsableQuotaContent = (quota?: QuotaStatusState) => { + if (!quota || quota.status !== 'success') return false; + const record = quota as unknown as Record; + const billing = record.billing; + return ['groups', 'windows', 'buckets', 'rows'].some((key) => { + const value = record[key]; + return Array.isArray(value) && value.length > 0; + }) || Boolean( + record.planType + || record.tierLabel + || record.creditBalance !== undefined + || (billing && typeof billing === 'object' && !Array.isArray(billing)) + ); +}; + +export const getQuotaForTarget = (store: QuotaStore, target: AccountQuotaTarget): QuotaStatusState | undefined => { + return target.config.storeSelector(store)[target.fileName] as QuotaStatusState | undefined; +}; + +export const requestAccountQuota = async ( + target: AccountQuotaTarget, + t: TFunction +): Promise => { + try { + const data = await target.config.fetchQuota(target.file, t); + return target.config.buildSuccessState(data) as QuotaStatusState; + } catch (err: unknown) { + const message = err instanceof Error ? err.message : t('common.unknown_error'); + return target.config.buildErrorState(message, getStatusFromError(err)) as QuotaStatusState; + } +}; + +export const settleWithConcurrency = async ( + items: T[], + concurrency: number, + worker: (item: T) => Promise +): Promise>> => { + const results = new Array>(items.length); + let nextIndex = 0; + const workerCount = Math.min(Math.max(concurrency, 1), items.length); + await Promise.all(Array.from({ length: workerCount }, async () => { + while (nextIndex < items.length) { + const index = nextIndex; + nextIndex += 1; + try { + results[index] = { status: 'fulfilled', value: await worker(items[index]) }; + } catch (reason) { + results[index] = { status: 'rejected', reason }; + } + } + })); + return results; +}; + +export const buildAccountQuotaTargetsByAccount = ( + rows: AccountQuotaSourceRow[], + authFilesByAuthIndex: Map +) => { + const grouped = new Map>(); + + rows.forEach((row) => { + const authIndex = normalizeAuthIndex(row.authIndex); + if (!authIndex || !row.account) return; + + const file = authFilesByAuthIndex.get(authIndex); + if (!file) return; + + const quotaConfig = getAccountQuotaConfig(file); + if (!quotaConfig) return; + + const dedupeKey = `${quotaConfig.type}::${authIndex}::${file.name}`; + const bucket = grouped.get(row.account) ?? new Map(); + if (!bucket.has(dedupeKey)) { + bucket.set(dedupeKey, { + key: dedupeKey, + authIndex, + authLabel: row.authLabel || file.name || authIndex, + fileName: file.name, + file, + config: quotaConfig, + }); + } + grouped.set(row.account, bucket); + }); + + return new Map( + Array.from(grouped.entries()).map(([account, bucket]) => [ + account, + Array.from(bucket.values()).sort((left, right) => left.authLabel.localeCompare(right.authLabel)), + ]) + ); +}; + +export const buildAccountQuotaEntriesByAccount = ( + targetsByAccount: Map, + quotaStore: QuotaStore, + t: TFunction +) => new Map( + Array.from(targetsByAccount.entries()).map(([account, targets]) => [ + account, + targets.map((target) => ({ + key: target.key, + authLabel: target.authLabel, + fileName: target.fileName, + providerLabel: getQuotaProviderLabel(target.config, t), + quota: getQuotaForTarget(quotaStore, target), + config: target.config, + } satisfies AccountQuotaEntry)), + ]) +); diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/AccountOverviewPanels.tsx b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/AccountOverviewPanels.tsx new file mode 100644 index 0000000..abd6285 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/AccountOverviewPanels.tsx @@ -0,0 +1,493 @@ +import { useState, type ReactNode } from 'react'; +import type { TFunction } from 'i18next'; +import { Card } from '@/components/ui/Card'; +import { IconChevronDown, IconChevronUp, IconRefreshCw } from '@/components/ui/icons'; +import type { QuotaRenderHelpers } from '@/components/quota/QuotaCard'; +import { QuotaProgressBar as AuthFileQuotaProgressBar } from '@/features/authFiles/components/QuotaProgressBar'; +import type { AccountStatusData } from '../accountHealth'; +import { + hasUsableQuotaContent, + resolveQuotaErrorMessage, + type AccountQuotaEntry, + type AccountQuotaState, +} from '../accountQuota'; +import { + formatPercent, + getAccountHealthTone, + type AccountHealthTone, +} from '../monitoringAnalytics'; +import { MonitoringHealthStatusBar } from './MonitoringHealthStatusBar'; +import { + joinUnique, + type MonitoringAccountRow, +} from '../hooks/useMonitoringData'; +import { formatCompactNumber, formatUsd } from '@/utils/usage'; +import authFileQuotaStyles from '@/pages/AuthFilesPage.module.scss'; +import styles from '../monitoring.module.scss'; + +type AccountSummaryMetric = { + key: string; + label: string; + value: ReactNode; + valueClassName?: string; +}; + +const SuccessFailureValue = ({ success, failure }: { success: number; failure: number }) => ( + + {formatCompactNumber(success)} + ({formatCompactNumber(failure)}) + +); + +const buildAccountCardFileName = (row: MonitoringAccountRow, quotaEntries: AccountQuotaEntry[] = []) => { + const quotaFileNames = Array.from(new Set(quotaEntries.map((entry) => entry.fileName).filter(Boolean))); + if (quotaFileNames.length > 0) return joinUnique(quotaFileNames, 1); + + const fileName = row.authLabels.find((label) => label && label !== '-' && label.endsWith('.json')); + return fileName || row.authLabels.find((label) => label && label !== '-') || row.accountMasked || row.account; +}; + +const buildAccountCardProviderText = (row: MonitoringAccountRow) => { + const providers = row.providers.filter((provider) => provider && provider !== '-'); + return providers.length > 0 ? joinUnique(providers, 2) : '-'; +}; + +const sortAccountOverviewCardMetrics = (metrics: AccountSummaryMetric[], t: TFunction) => { + const labels: Record = { + 'total-tokens': t('monitoring.token_metric_total'), + 'input-tokens': t('monitoring.token_metric_input'), + 'output-tokens': t('monitoring.token_metric_output'), + 'cached-tokens': t('monitoring.token_metric_cached'), + }; + const order = ['total-tokens', 'input-tokens', 'output-tokens', 'cached-tokens']; + return order + .map((key) => { + const metric = metrics.find((item) => item.key === key); + return metric ? { ...metric, label: labels[key] } : undefined; + }) + .filter(Boolean) as AccountSummaryMetric[]; +}; + +const buildAccountSummaryMetrics = ( + row: MonitoringAccountRow, + hasPrices: boolean, + locale: string, + t: TFunction +): AccountSummaryMetric[] => [ + { + key: 'total-calls', + label: t('monitoring.total_calls'), + value: formatCompactNumber(row.totalCalls), + }, + { + key: 'success-calls', + label: t('monitoring.success_calls'), + value: , + }, + { + key: 'success-rate', + label: t('monitoring.call_success_rate'), + value: formatPercent(row.successRate), + valueClassName: + row.successRate >= 0.95 + ? styles.goodText + : row.successRate >= 0.85 + ? styles.warnText + : styles.badText, + }, + { + key: 'total-tokens', + label: t('monitoring.total_tokens'), + value: formatCompactNumber(row.totalTokens), + }, + { + key: 'input-tokens', + label: t('monitoring.input_tokens'), + value: formatCompactNumber(row.inputTokens), + }, + { + key: 'output-tokens', + label: t('monitoring.output_tokens'), + value: formatCompactNumber(row.outputTokens), + }, + { + key: 'cached-tokens', + label: t('monitoring.cached_tokens'), + value: formatCompactNumber(row.cachedTokens), + }, + { + key: 'estimated-cost', + label: t('monitoring.estimated_cost'), + value: hasPrices ? formatUsd(row.totalCost) : '--', + }, + { + key: 'latest-request-time', + label: t('monitoring.latest_request_time'), + value: new Date(row.lastSeenAt).toLocaleString(locale), + }, +]; + +const ACCOUNT_QUOTA_RENDER_HELPERS: QuotaRenderHelpers = { + styles: { + ...authFileQuotaStyles, + quotaRow: `${authFileQuotaStyles.quotaRow} ${styles.accountQuotaRow}`, + quotaRowHeader: `${authFileQuotaStyles.quotaRowHeader} ${styles.accountQuotaRowHeader}`, + quotaModel: `${authFileQuotaStyles.quotaModel} ${styles.accountQuotaModel}`, + quotaMeta: `${authFileQuotaStyles.quotaMeta} ${styles.accountQuotaMeta}`, + quotaAmount: `${authFileQuotaStyles.quotaAmount} ${styles.accountQuotaAmount}`, + codexPlanValue: `${authFileQuotaStyles.codexPlanValue} ${styles.accountQuotaPlanValue}`, + premiumPlanValue: `${authFileQuotaStyles.premiumPlanValue} ${styles.accountQuotaPremiumPlanValue}`, + codexResetCreditRow: `${authFileQuotaStyles.codexResetCreditRow} ${styles.accountQuotaResetCreditRow}`, + codexResetCreditTime: `${authFileQuotaStyles.codexResetCreditTime} ${styles.accountQuotaResetCreditTime}`, + }, + QuotaProgressBar: AuthFileQuotaProgressBar, +}; + +const getSuccessRateClassName = (rate: number) => + rate >= 0.95 ? styles.goodText : rate >= 0.85 ? styles.warnText : styles.badText; + +const getAccountStatusDotClassName = (tone: AccountHealthTone) => { + if (tone === 'good') return styles.accountStatusDotEnabled; + if (tone === 'warn') return styles.accountStatusDotMixed; + return styles.accountStatusDotDisabled; +}; + +function AccountHealthStatusPanel({ + row, + hasPrices, + locale, + t, + statusData, + scopeText, +}: { + row: MonitoringAccountRow; + hasPrices: boolean; + locale: string; + t: TFunction; + statusData: AccountStatusData; + scopeText: string; +}) { + const healthMetrics = [ + { key: 'total-calls', label: t('monitoring.total_calls'), value: formatCompactNumber(row.totalCalls) }, + { + key: 'success-failure', + label: t('monitoring.success_failure'), + value: , + }, + { key: 'estimated-cost', label: t('monitoring.estimated_cost'), value: hasPrices ? formatUsd(row.totalCost) : '--', className: styles.primaryText }, + { key: 'success-rate', label: t('monitoring.success_rate'), value: formatPercent(row.successRate), className: getSuccessRateClassName(row.successRate) }, + ]; + + return ( +
+
+ {t('monitoring.account_health_status')} + + i + +
+
+ {healthMetrics.map((metric) => ( +
+ {metric.label} + {metric.value} +
+ ))} +
+ +
{scopeText}
+
+ ); +} + +function AccountTokenMetricGrid({ metrics, t }: { metrics: AccountSummaryMetric[]; t: TFunction }) { + const getTokenMetricToneClassName = (key: string) => { + if (key === 'input-tokens') return styles.accountMetricIconInput; + if (key === 'output-tokens') return styles.accountMetricIconOutput; + if (key === 'cached-tokens') return styles.accountMetricIconCached; + return styles.accountMetricIconTotal; + }; + + return ( +
+
+ {t('monitoring.token_usage')} +
+
+ {metrics.map((metric) => ( +
+ + + + {metric.value} + +
+ ))} +
+
+ ); +} + +function AccountModelUsageList({ + row, + hasPrices, + locale, + t, + limit = 1, +}: { + row: MonitoringAccountRow; + hasPrices: boolean; + locale: string; + t: TFunction; + limit?: number; +}) { + const [showAll, setShowAll] = useState(false); + const [expandedModels, setExpandedModels] = useState>({}); + const hasExtraModels = row.models.length > limit; + const visibleModels = showAll ? row.models : row.models.slice(0, limit); + const toggleModel = (key: string) => setExpandedModels((previous) => ({ ...previous, [key]: !previous[key] })); + + return ( +
+
+ {t('monitoring.top_models')} + {hasExtraModels ? ( + + ) : null} +
+ + {visibleModels.length > 0 ? ( +
+ {visibleModels.map((model) => { + const modelKey = `${row.id}-${model.model}`; + const isModelExpanded = Boolean(expandedModels[modelKey]); + return ( +
+ + {isModelExpanded ? ( +
+
{t('monitoring.input_tokens')}{formatCompactNumber(model.inputTokens)}
+
{t('monitoring.output_tokens')}{formatCompactNumber(model.outputTokens)}
+
{t('monitoring.cached_tokens')}{formatCompactNumber(model.cachedTokens)}
+
{t('monitoring.latest_request_time')}{new Date(model.lastSeenAt).toLocaleString(locale)}
+
+ ) : null} +
+ ); + })} +
+ ) : ( +
{t('monitoring.no_model_data')}
+ )} +
+ ); +} + +function AccountQuotaPanel({ + quotaState, + quotaEntries, + locale, + t, + onRefreshQuota, +}: { + quotaState?: AccountQuotaState; + quotaEntries: AccountQuotaEntry[]; + locale: string; + t: TFunction; + onRefreshQuota: () => void; +}) { + const quotaLoading = quotaState?.status === 'loading'; + const lastQuotaSync = quotaState?.lastRefreshedAt && Number.isFinite(quotaState.lastRefreshedAt) + ? new Date(quotaState.lastRefreshedAt).toLocaleString(locale) + : ''; + const quotaTitle = quotaEntries.length === 1 ? quotaEntries[0].providerLabel : t('quota_management.title'); + + const renderRefreshButton = () => ( + + ); + + return ( +
+
+
+ {quotaTitle} + {lastQuotaSync ? {`${t('monitoring.last_sync')}: ${lastQuotaSync}`} : null} +
+ {renderRefreshButton()} +
+ + {quotaLoading && quotaEntries.length === 0 ?
{t('codex_quota.loading')}
: null} + {!quotaLoading && quotaState?.status === 'error' && quotaEntries.length === 0 ? ( +
{t('codex_quota.load_failed', { message: quotaState.error || t('common.unknown_error') })}
+ ) : null} + {!quotaLoading && quotaState?.status === 'success' && quotaEntries.length === 0 ?
{t('monitoring.account_quota_empty')}
: null} + {!quotaState && quotaEntries.length === 0 ?
{t('monitoring.account_quota_empty')}
: null} + + {quotaEntries.length > 0 ? ( +
+ {quotaEntries.map((entry) => ( +
+
+
+ {entry.authLabel} +
+
+ + {entry.quota?.status === 'loading' ? ( +
{t(`${entry.config.i18nPrefix}.loading`)}
+ ) : entry.quota?.status === 'error' ? ( +
+ {t(`${entry.config.i18nPrefix}.load_failed`, { message: resolveQuotaErrorMessage(t, entry.quota) })} +
+ ) : hasUsableQuotaContent(entry.quota) ? ( +
+ {entry.config.renderQuotaItems(entry.quota!, t, ACCOUNT_QUOTA_RENDER_HELPERS)} +
+ ) : ( +
{t(`${entry.config.i18nPrefix}.idle`)}
+ )} +
+ ))} +
+ ) : null} +
+ ); +} + +function AccountExpandedDetails({ + row, + hasPrices, + locale, + t, + quotaState, + quotaEntries, + onRefreshQuota, +}: { + row: MonitoringAccountRow; + hasPrices: boolean; + locale: string; + t: TFunction; + quotaState?: AccountQuotaState; + quotaEntries: AccountQuotaEntry[]; + onRefreshQuota: () => void; +}) { + return ( +
+ + +
+ ); +} + +export function AccountOverviewCard({ + row, + hasPrices, + locale, + t, + isExpanded, + statusData, + scopeText, + quotaState, + quotaEntries, + onToggle, + onRefreshQuota, +}: { + row: MonitoringAccountRow; + hasPrices: boolean; + locale: string; + t: TFunction; + isExpanded: boolean; + statusData: AccountStatusData; + scopeText: string; + quotaState?: AccountQuotaState; + quotaEntries: AccountQuotaEntry[]; + onToggle: () => void; + onRefreshQuota: () => void; +}) { + const summaryMetrics = buildAccountSummaryMetrics(row, hasPrices, locale, t); + const cardMetrics = sortAccountOverviewCardMetrics(summaryMetrics, t); + const tone = getAccountHealthTone(row); + const latestRequestText = new Date(row.lastSeenAt).toLocaleString(locale); + const accountLabel = buildAccountCardFileName(row, quotaEntries); + const providerText = buildAccountCardProviderText(row); + + return ( + +
+
+ + + {tone === 'good' ? t('monitoring.health_good') : tone === 'warn' ? t('monitoring.health_warn') : t('monitoring.health_bad')} + +
+
+ {providerText} + · + {t('monitoring.latest_request_time_value', { value: latestRequestText })} +
+
+ + + + + {isExpanded ? ( + + ) : null} +
+ ); +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/AccountStatsPanel.tsx b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/AccountStatsPanel.tsx new file mode 100644 index 0000000..92bb3a2 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/AccountStatsPanel.tsx @@ -0,0 +1,275 @@ +import { useCallback, useEffect, useMemo, useState, type ChangeEvent } from 'react'; +import type { TFunction } from 'i18next'; +import { Button } from '@/components/ui/Button'; +import { Card } from '@/components/ui/Card'; +import { Input } from '@/components/ui/Input'; +import { IconSearch } from '@/components/ui/icons'; +import { buildAccountStatusData, buildAccountStatusRange } from '../accountHealth'; +import type { AccountQuotaEntry, AccountQuotaState } from '../accountQuota'; +import { getAccountHealthTone, type AccountHealthTone, type AccountSortMetric } from '../monitoringAnalytics'; +import { ACCOUNT_SORT_OPTIONS, TIME_RANGE_OPTIONS } from '../monitoringOptions'; +import type { MonitoringAccountRow, MonitoringTimeRange } from '../hooks/useMonitoringData'; +import { AccountOverviewCard } from './AccountOverviewPanels'; +import quotaStyles from '@/pages/QuotaPage.module.scss'; +import styles from '../monitoring.module.scss'; + +const formatAccountOverviewScopeText = (rangeLabel: string, t: TFunction) => ( + t('monitoring.account_scope_text', { range: rangeLabel }) +); + +export function AccountStatsPanel({ + rows, + metric, + emptyText, + hasPrices, + locale, + t, + rangeLabel, + range, + onRangeChange, + onHide, + expandedAccounts, + accountQuotaStates, + accountQuotaEntriesByAccount, + onMetricChange, + onToggleAccount, + onRefreshQuota, +}: { + rows: MonitoringAccountRow[]; + metric: AccountSortMetric; + emptyText: string; + hasPrices: boolean; + locale: string; + t: TFunction; + rangeLabel: string; + range: MonitoringTimeRange; + onRangeChange: (range: MonitoringTimeRange) => void; + onHide: () => void; + expandedAccounts: Record; + accountQuotaStates: Record; + accountQuotaEntriesByAccount: Map; + onMetricChange: (metric: AccountSortMetric) => void; + onToggleAccount: (accountId: string, account: string) => void; + onRefreshQuota: (account: string) => void; +}) { + const ACCOUNT_CARD_MIN_WIDTH = 330; + const ACCOUNT_CARD_GAP = 16; + const ROWS_PER_PAGE = 2; + + const [cardPage, setCardPage] = useState(0); + const [gridEl, setGridEl] = useState(null); + const gridRef = useCallback((el: HTMLDivElement | null) => setGridEl(el), []); + const [gridCols, setGridCols] = useState(3); + const [accountSearch, setAccountSearch] = useState(''); + const [accountProviderFilter, setAccountProviderFilter] = useState('all'); + const [accountHealthFilter, setAccountHealthFilter] = useState<'all' | AccountHealthTone>('all'); + + useEffect(() => { + if (!gridEl) return; + const update = () => { + const cols = Math.max(1, Math.floor((gridEl.clientWidth + ACCOUNT_CARD_GAP) / (ACCOUNT_CARD_MIN_WIDTH + ACCOUNT_CARD_GAP))); + setGridCols((current) => (current === cols ? current : cols)); + }; + update(); + const observer = new ResizeObserver(update); + observer.observe(gridEl); + return () => observer.disconnect(); + }, [gridEl]); + + const providerOptions = useMemo(() => { + const set = new Set(); + rows.forEach((row) => row.providers.forEach((p) => { if (p && p !== '-') set.add(p); })); + return Array.from(set).sort(); + }, [rows]); + + const filteredRows = useMemo(() => { + const query = accountSearch.trim().toLowerCase(); + return rows.filter((row) => { + if (query) { + const haystack = [row.accountMasked, row.account, ...row.authLabels, ...row.providers].join(' ').toLowerCase(); + if (!haystack.includes(query)) return false; + } + if (accountProviderFilter !== 'all') { + if (!row.providers.includes(accountProviderFilter)) return false; + } + if (accountHealthFilter !== 'all') { + if (getAccountHealthTone(row) !== accountHealthFilter) return false; + } + return true; + }); + }, [rows, accountSearch, accountProviderFilter, accountHealthFilter]); + + const hasActiveFilters = accountSearch.trim() !== '' || accountProviderFilter !== 'all' || accountHealthFilter !== 'all'; + + const itemsPerPage = gridCols * ROWS_PER_PAGE; + const totalPages = Math.max(1, Math.ceil(filteredRows.length / itemsPerPage)); + const safePageIndex = Math.min(cardPage, totalPages - 1); + const visibleRows = useMemo( + () => filteredRows.slice(safePageIndex * itemsPerPage, (safePageIndex + 1) * itemsPerPage), + [filteredRows, itemsPerPage, safePageIndex] + ); + + const accountStatusRange = useMemo( + () => buildAccountStatusRange(rows, range), + [rows, range] + ); + + const accountStatusDataById = useMemo(() => { + const entries = visibleRows.map((row) => [row.id, buildAccountStatusData(row.rows ?? [], accountStatusRange)] as const); + return new Map(entries); + }, [accountStatusRange, visibleRows]); + + useEffect(() => { + setCardPage(0); + }, [accountSearch, accountProviderFilter, accountHealthFilter, metric, range, itemsPerPage]); + + return ( + <> +
+
+

{t('monitoring.account_stats_title')}

+

{t('monitoring.account_stats_desc')}

+
+ +
+
+ {TIME_RANGE_OPTIONS.map((option) => ( + + ))} +
+ +
+
+ + +
+
+ ) => setAccountSearch(event.target.value)} + placeholder={t('monitoring.search_account')} + className={styles.accountStatsSearchInput} + rightElement={} + aria-label={t('monitoring.search_account')} + /> + {providerOptions.length > 0 && ( + + )} + + {hasActiveFilters && ( + + )} +
+
+ {ACCOUNT_SORT_OPTIONS.map((option) => ( + + ))} +
+
+ + {filteredRows.length > 0 ? ( + <> +
+ {visibleRows.map((row) => { + const statusData = accountStatusDataById.get(row.id) ?? buildAccountStatusData([], accountStatusRange); + return ( + onToggleAccount(row.id, row.account)} + onRefreshQuota={() => onRefreshQuota(row.account)} + /> + ); + })} +
+ {totalPages > 1 && ( +
+ +
+ {t('auth_files.pagination_info', { + current: safePageIndex + 1, + total: totalPages, + count: filteredRows.length, + defaultValue: `${safePageIndex + 1} / ${totalPages} · ${filteredRows.length}`, + })} +
+ +
+ )} + + ) : ( +
{hasActiveFilters ? t('monitoring.no_matching_accounts') : emptyText}
+ )} +
+ + ); +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/ModelPriceManagerModal.tsx b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/ModelPriceManagerModal.tsx new file mode 100644 index 0000000..451ed4d --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/ModelPriceManagerModal.tsx @@ -0,0 +1,582 @@ +import { useMemo, type Dispatch, type SetStateAction } from 'react'; +import type { TFunction } from 'i18next'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { Modal } from '@/components/ui/Modal'; +import { IconRefreshCw, IconSearch, IconTrash2 } from '@/components/ui/icons'; +import { formatShortDateTime } from '../hooks/useMonitoringData'; +import { + MODEL_PRICE_SYNC_RATE_FIELDS, + createPriceDraft, + formatModelPriceRate, + type PriceDraft, + type PriceManagementView, + type PriceRuleTarget, + type PriceSyncChangeFilter, + type PriceTierDraft, +} from '../modelPricePresentation'; +import type { MonitoringSettingsDraft } from '../monitoringSettings'; +import { + formatCompactNumber, + type ModelPriceSyncResult, + type ModelPriceSyncState, +} from '@/utils/usage'; +import styles from '../monitoring.module.scss'; + +export function ModelPriceManagerModal({ + isPriceModalOpen, + setIsPriceModalOpen, + priceManagementView, + setPriceManagementView, + priceRuleTargets, + priceRuleSearch, + setPriceRuleSearch, + priceModel, + selectPriceTarget, + isPriceLoading, + priceDraft, + setPriceDraft, + handlePriceDraftChange, + handlePriceTierChange, + addPriceTier, + removePriceTier, + handleDeletePrice, + handleSavePrice, + isPriceSaving, + priceSyncState, + priceSyncResult, + isPriceSyncing, + handleSyncPrices, + priceSyncLockedOverrides, + setPriceSyncLockedOverrides, + priceSyncChangeFilter, + setPriceSyncChangeFilter, + monitoringSettingsDraft, + setMonitoringSettingsDraft, + handleSaveMonitoringSettings, + isMonitoringSettingsLoading, + isMonitoringSettingsSaving, + t, +}: { + isPriceModalOpen: boolean; + setIsPriceModalOpen: Dispatch>; + priceManagementView: PriceManagementView; + setPriceManagementView: Dispatch>; + priceRuleTargets: PriceRuleTarget[]; + priceRuleSearch: string; + setPriceRuleSearch: Dispatch>; + priceModel: string; + selectPriceTarget: (model: string) => void; + isPriceLoading: boolean; + priceDraft: PriceDraft; + setPriceDraft: Dispatch>; + handlePriceDraftChange: (field: Exclude, value: string) => void; + handlePriceTierChange: (index: number, field: keyof PriceTierDraft, value: string) => void; + addPriceTier: () => void; + removePriceTier: (index: number) => void; + handleDeletePrice: (model: string) => void | Promise; + handleSavePrice: () => void | Promise; + isPriceSaving: boolean; + priceSyncState: ModelPriceSyncState; + priceSyncResult: ModelPriceSyncResult | null; + isPriceSyncing: boolean; + handleSyncPrices: (dryRun?: boolean) => void | Promise; + priceSyncLockedOverrides: string[]; + setPriceSyncLockedOverrides: Dispatch>; + priceSyncChangeFilter: PriceSyncChangeFilter; + setPriceSyncChangeFilter: Dispatch>; + monitoringSettingsDraft: MonitoringSettingsDraft; + setMonitoringSettingsDraft: Dispatch>; + handleSaveMonitoringSettings: (closeModal?: boolean) => void | Promise; + isMonitoringSettingsLoading: boolean; + isMonitoringSettingsSaving: boolean; + t: TFunction; +}) { + const filteredPriceRuleTargets = useMemo(() => { + const query = priceRuleSearch.trim().toLowerCase(); + if (!query) return priceRuleTargets; + return priceRuleTargets.filter((item) => item.model.toLowerCase().includes(query)); + }, [priceRuleSearch, priceRuleTargets]); + const selectedPriceTarget = useMemo( + () => priceRuleTargets.find((item) => item.model === priceModel), + [priceModel, priceRuleTargets] + ); + const configuredPriceRuleCount = priceRuleTargets.filter((item) => Boolean(item.rule)).length; + const unconfiguredPriceRuleCount = priceRuleTargets.length - configuredPriceRuleCount; + const priceSyncStatus = isPriceSyncing ? 'syncing' : priceSyncState.status; + const unmatchedPriceModels = priceSyncResult?.unmatched ?? priceSyncState.unmatchedModels ?? []; + const unmatchedPriceModelCount = priceSyncResult + ? unmatchedPriceModels.length + : (priceSyncState.unmatched ?? unmatchedPriceModels.length); + const priceSyncChanges = useMemo(() => priceSyncResult?.changes ?? [], [priceSyncResult]); + const priceSyncLockedOverrideSet = useMemo( + () => new Set(priceSyncLockedOverrides), + [priceSyncLockedOverrides] + ); + const priceSyncChangeCounts = useMemo(() => { + const counts = { added: 0, updated: 0, overridden: 0, locked: 0, unmatched: 0 }; + priceSyncChanges.forEach((change) => { + const action = change.action === 'locked' && priceSyncLockedOverrideSet.has(change.model) + ? 'overridden' + : change.action; + counts[action] += 1; + }); + return counts; + }, [priceSyncChanges, priceSyncLockedOverrideSet]); + const filteredPriceSyncChanges = useMemo( + () => priceSyncChangeFilter === 'all' + ? priceSyncChanges + : priceSyncChanges.filter((change) => { + const action = change.action === 'locked' && priceSyncLockedOverrideSet.has(change.model) + ? 'overridden' + : change.action; + return action === priceSyncChangeFilter; + }), + [priceSyncChangeFilter, priceSyncChanges, priceSyncLockedOverrideSet] + ); + const lockedPriceSyncChanges = useMemo( + () => priceSyncChanges.filter((change) => change.action === 'locked'), + [priceSyncChanges] + ); + const allLockedPriceSyncChangesSelected = lockedPriceSyncChanges.length > 0 + && lockedPriceSyncChanges.every((change) => priceSyncLockedOverrideSet.has(change.model)); + + return ( + setIsPriceModalOpen(false)} + title={t('usage_stats.model_price_settings')} + width={960} + className={`${styles.monitorModal} ${styles.priceManagerModal}`} + > +
+
+ + +
+ + {priceManagementView === 'rules' ? ( +
+ + +
+ {selectedPriceTarget ? ( + <> +
+
+

{selectedPriceTarget.model}

+ {t('usage_stats.model_price_model_scope')} +
+
+ + {t(selectedPriceTarget.rule ? 'usage_stats.model_price_configured' : 'usage_stats.model_price_unconfigured')} + + {selectedPriceTarget.rule?.source ? {selectedPriceTarget.rule.source} : null} +
+
+ +
+
+
+

{t('usage_stats.model_price_base_rates')}

+ USD / 1M +
+
+ {([ + ['input', 'usage_stats.model_price_input'], + ['output', 'usage_stats.model_price_output'], + ['cacheRead', 'usage_stats.model_price_cache_read'], + ['cacheWrite', 'usage_stats.model_price_cache_write'], + ] as const).map(([field, label]) => ( + + ))} +
+
+ +
+
+
+

{t('usage_stats.model_price_context_tier')}

+ {t('usage_stats.model_price_tier_count', { count: priceDraft.tiers.length })} +
+ +
+
+ {priceDraft.tiers.map((tier, index) => ( +
+ {index + 1} + + + + + + +
+ ))} + {priceDraft.tiers.length === 0 ? ( +
{t('usage_stats.model_price_tier_empty')}
+ ) : null} +
+
+
+ +
+
+ {selectedPriceTarget.rule ? ( + + ) : null} +
+
+ + +
+
+ + ) : ( +
{t('usage_stats.model_price_select_empty')}
+ )} +
+
+ ) : ( +
+
+
+ +
+

{t(`usage_stats.model_price_sync_state_${priceSyncStatus}`, { defaultValue: priceSyncStatus })}

+ + {priceSyncState.lastSuccessMs + ? t('usage_stats.model_price_last_sync', { value: formatShortDateTime(priceSyncState.lastSuccessMs) }) + : t('usage_stats.model_price_sync_never')} + +
+
+
+ + +
+
+ +
+ {([ + ['matched', priceSyncResult?.matched ?? priceSyncState.matched ?? 0], + ['added', priceSyncResult?.added ?? priceSyncState.added ?? 0], + ['updated', priceSyncResult?.updated ?? priceSyncState.updated ?? 0], + ['unmatched', unmatchedPriceModelCount], + ] as const).map(([key, value]) => ( +
+ {t(`usage_stats.model_price_sync_metric_${key}`)} + {formatCompactNumber(value)} +
+ ))} +
+ + {priceSyncState.error ?
{priceSyncState.error}
: null} + + {priceSyncResult ? ( +
+
+
+

{t(priceSyncResult.dryRun ? 'usage_stats.model_price_sync_preview_details' : 'usage_stats.model_price_sync_applied_details')}

+ {t('usage_stats.model_price_sync_change_summary', { + added: priceSyncChangeCounts.added, + updated: priceSyncChangeCounts.updated, + overridden: priceSyncChangeCounts.overridden, + locked: priceSyncChangeCounts.locked, + unmatched: unmatchedPriceModelCount, + })} +
+
+ {lockedPriceSyncChanges.length > 0 ? ( + + ) : null} +
+ {(['all', 'added', 'updated', 'overridden', 'locked', 'unmatched'] as const).map((filter) => { + const count = filter === 'all' ? priceSyncChanges.length : priceSyncChangeCounts[filter]; + if (filter !== 'all' && count === 0) return null; + const filterLabel = filter === 'overridden' && priceSyncResult.dryRun + ? t('usage_stats.model_price_sync_override_selected') + : t(`usage_stats.model_price_sync_change_${filter}`); + return ( + + ); + })} +
+
+
+ +
+ {filteredPriceSyncChanges.map((change) => { + const rateChanges = MODEL_PRICE_SYNC_RATE_FIELDS.filter(([field]) => ( + change.after && (!change.before || change.before.base[field] !== change.after.base[field]) + )); + const beforeTierCount = change.before?.tiers?.length ?? 0; + const afterTierCount = change.after?.tiers?.length ?? 0; + const overrideSelected = change.action === 'locked' && priceSyncLockedOverrideSet.has(change.model); + const displayedAction = overrideSelected ? 'overridden' : change.action; + return ( +
+
+ + {overrideSelected + ? t('usage_stats.model_price_sync_override_selected') + : t(`usage_stats.model_price_sync_change_${displayedAction}`)} + +
+ {change.model} + + {change.sourceProvider + ? `${change.sourceProvider}/${change.sourceModel || change.model}` + : t('usage_stats.model_price_sync_change_no_source')} + {' · '} + {t('usage_stats.model_price_requests', { count: change.requests })} + +
+
+ + {change.action !== 'unmatched' ? ( +
+ {rateChanges.map(([field, label]) => ( +
+ {t(label)} +
+ {change.before ? {formatModelPriceRate(change.before.base[field])} : null} + {change.before ? : null} + {formatModelPriceRate(change.after?.base[field])} +
+
+ ))} + {beforeTierCount !== afterTierCount ? ( +
+ {t('usage_stats.model_price_context_tier')} +
+ {beforeTierCount} + + {afterTierCount} +
+
+ ) : null} + {rateChanges.length === 0 && beforeTierCount === afterTierCount ? ( + {t(change.action === 'locked' + ? overrideSelected + ? 'usage_stats.model_price_sync_override_selected_hint' + : 'usage_stats.model_price_sync_change_locked_hint' + : 'usage_stats.model_price_sync_change_metadata_hint')} + ) : null} + {change.action === 'locked' ? ( + + ) : null} +
+ ) : ( + {t('usage_stats.model_price_sync_change_unmatched_hint')} + )} +
+ ); + })} + {filteredPriceSyncChanges.length === 0 ? ( +
{t('usage_stats.model_price_sync_no_changes')}
+ ) : null} +
+
+ ) : unmatchedPriceModelCount > 0 ? ( +
+
+
+

{t('usage_stats.model_price_sync_unmatched')}

+ {unmatchedPriceModelCount} +
+
+
+ {unmatchedPriceModels.map((item) => ( +
+ + {item.model} + {item.alias ? {item.alias} : null} + + {t('usage_stats.model_price_requests', { count: item.requests })} +
+ ))} +
+
+ ) : null} + +
+
+ {t('usage_stats.model_price_sync_schedule_title')} + {t('usage_stats.model_price_sync_schedule_desc')} +
+
+ + + +
+
+
+ )} +
+
+ ); +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/MonitoringHealthStatusBar.tsx b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/MonitoringHealthStatusBar.tsx index b3c4a08..3504d55 100644 --- a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/MonitoringHealthStatusBar.tsx +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/MonitoringHealthStatusBar.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useId, useRef, useState } from 'react'; import type { TFunction } from 'i18next'; import type { AccountStatusBlockDetail, AccountStatusData } from '../accountHealth'; -import styles from '@/pages/MonitoringCenterPage.module.scss'; +import styles from '../monitoring.module.scss'; diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/MonitoringSettingsModal.tsx b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/MonitoringSettingsModal.tsx new file mode 100644 index 0000000..3fddd1f --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/MonitoringSettingsModal.tsx @@ -0,0 +1,167 @@ +import type { Dispatch, SetStateAction } from 'react'; +import type { TFunction } from 'i18next'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { Modal } from '@/components/ui/Modal'; +import { IconTrash2 } from '@/components/ui/icons'; +import { formatCompactNumber } from '@/utils/usage'; +import type { MonitoringSettingsDraft } from '../monitoringSettings'; +import styles from '../monitoring.module.scss'; + +export function MonitoringSettingsModal({ + isMonitoringSettingsOpen, + setIsMonitoringSettingsOpen, + monitoringSettingsDraft, + setMonitoringSettingsDraft, + usageTotalRequests, + isMonitoringStatisticsResetting, + isMonitoringSettingsSaving, + handleMonitoringStatisticsReset, + handleSaveMonitoringSettings, + t, +}: { + isMonitoringSettingsOpen: boolean; + setIsMonitoringSettingsOpen: Dispatch>; + monitoringSettingsDraft: MonitoringSettingsDraft; + setMonitoringSettingsDraft: Dispatch>; + usageTotalRequests: number; + isMonitoringStatisticsResetting: boolean; + isMonitoringSettingsSaving: boolean; + handleMonitoringStatisticsReset: () => void; + handleSaveMonitoringSettings: () => void | Promise; + t: TFunction; +}) { + return ( + { + if (!isMonitoringStatisticsResetting) setIsMonitoringSettingsOpen(false); + }} + title={t('usage_stats.monitoring_settings')} + width={760} + className={styles.monitorModal} + > +
+
+
+ {t('usage_stats.monitoring_settings_retention_title')} + {t('usage_stats.monitoring_settings_retention_desc')} +
+ +
+ +
+
+ {t('usage_stats.monitoring_settings_webdav_title')} + {t('usage_stats.monitoring_settings_webdav_desc')} +
+ +
+ + + + + +
+ {t('usage_stats.monitoring_settings_webdav_hint')} +
+ +
+
+ {t('usage_stats.monitoring_settings_data_title')} + {t('usage_stats.monitoring_settings_data_desc')} +
+
+
+ {t('usage_stats.monitoring_settings_data_count')} + {formatCompactNumber(usageTotalRequests)} +
+ +
+
+ +
+ + +
+
+
+ ); +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/RealtimeCostCell.tsx b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/RealtimeCostCell.tsx index 3d2e3ad..904af94 100644 --- a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/RealtimeCostCell.tsx +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/RealtimeCostCell.tsx @@ -10,7 +10,7 @@ import type { TFunction } from 'i18next'; import { IconInfo } from '@/components/ui/icons'; import type { RealtimeLogRow } from '../realtimeLogPresentation'; import { formatCompactNumber, formatUsdPrecise } from '@/utils/usage'; -import styles from '@/pages/MonitoringCenterPage.module.scss'; +import styles from '../monitoring.module.scss'; type RealtimeCostTooltipPosition = { top: number; diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/RealtimeLogDetails.tsx b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/RealtimeLogDetails.tsx new file mode 100644 index 0000000..7cc9536 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/RealtimeLogDetails.tsx @@ -0,0 +1,102 @@ +import type { ReactNode } from 'react'; +import type { TFunction } from 'i18next'; +import type { MonitoringStatusTone } from '../hooks/useMonitoringData'; +import { + buildRealtimeStatusLabel, + compactRealtimeErrorMessage, + translateRealtimeErrorCategory, + translateRealtimeErrorText, + type RealtimeLogRow, +} from '../realtimeLogPresentation'; +import { maskSensitiveText } from '@/utils/format'; +import styles from '../monitoring.module.scss'; + +export function StatusBadge({ tone, children }: { tone: MonitoringStatusTone; children: ReactNode }) { + return {children}; +} + +export function RealtimeErrorDetailsPanel({ + row, + t, + language, +}: { + row: RealtimeLogRow; + t: TFunction; + language?: string; +}) { + const categoryText = translateRealtimeErrorCategory(row.errorCategoryKey, t, language); + const statusText = buildRealtimeStatusLabel(row, t('monitoring.result_failed')); + const summaryText = row.errorMessage + ? compactRealtimeErrorMessage(row.errorMessage, 220) + : row.errorSummary || row.diagnosticText || categoryText; + const detailItems = [ + { label: translateRealtimeErrorText('http_status', t, language), value: row.statusCode !== null ? String(row.statusCode) : '-' }, + { label: translateRealtimeErrorText('error_code', t, language), value: row.errorCode || '-' }, + { label: translateRealtimeErrorText('upstream_request_id', t, language), value: row.upstreamRequestId || '-' }, + { label: translateRealtimeErrorText('retry_after', t, language), value: row.retryAfter || '-' }, + ].filter((item) => item.value !== '-'); + + return ( +
+
+
+ {statusText} + {categoryText} +
+ {summaryText} +
+ {row.errorMessage ? ( +
+ {translateRealtimeErrorText('error_message', t, language)} +
{compactRealtimeErrorMessage(row.errorMessage, 1200)}
+
+ ) : null} + {detailItems.length > 0 ? ( +
+ {detailItems.map((item) => ( +
+ {item.label} + {maskSensitiveText(item.value)} +
+ ))} +
+ ) : null} +
+ ); +} + +export function RecentPattern({ + pattern, + variant = 'default', + label, +}: { + pattern: boolean[]; + variant?: 'default' | 'plain'; + label?: string; +}) { + const normalized = pattern.length > 0 ? pattern : Array.from({ length: 10 }, () => true); + const successCount = normalized.filter(Boolean).length; + const failureCount = normalized.length - successCount; + const ariaLabel = label ?? `Recent ${normalized.length} requests: ${successCount} succeeded, ${failureCount} failed`; + const containerClassName = [ + styles.patternBars, + variant === 'plain' ? styles.patternBarsPlain : '', + ] + .filter(Boolean) + .join(' '); + const barClassName = [styles.patternBar, variant === 'plain' ? styles.patternBarPlain : ''] + .filter(Boolean) + .join(' '); + + return ( +
+ {normalized.map((item, index) => ( +
+ ); +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/UsageAnalyticsPanels.tsx b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/UsageAnalyticsPanels.tsx index ccaee3d..137f879 100644 --- a/cliproxyapi-pro-management/overlay/src/features/monitoring/components/UsageAnalyticsPanels.tsx +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/components/UsageAnalyticsPanels.tsx @@ -17,7 +17,7 @@ import { } from '../monitoringAnalytics'; import { RANKING_METRIC_OPTIONS, TIME_RANGE_OPTIONS } from '../monitoringOptions'; import { formatCompactNumber, formatUsd } from '@/utils/usage'; -import styles from '@/pages/MonitoringCenterPage.module.scss'; +import styles from '../monitoring.module.scss'; export type UsageMetricCard = { key: string; diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/modelPricePresentation.ts b/cliproxyapi-pro-management/overlay/src/features/monitoring/modelPricePresentation.ts new file mode 100644 index 0000000..bdbc272 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/modelPricePresentation.ts @@ -0,0 +1,77 @@ +import type { + ModelPriceRule, + ModelPriceSyncChangeAction, +} from '@/utils/usage'; + +export type PriceTierDraft = { + contextSize: string; + input: string; + output: string; + cacheRead: string; + cacheWrite: string; +}; + +export type PriceDraft = { + input: string; + output: string; + cacheRead: string; + cacheWrite: string; + tiers: PriceTierDraft[]; +}; + +export type PriceManagementView = 'rules' | 'sync'; +export type PriceSyncChangeFilter = 'all' | ModelPriceSyncChangeAction; + +export type PriceRuleTarget = { + key: string; + model: string; + requests: number; + lastSeenAtMs: number; + rule?: ModelPriceRule; +}; + +const roundCurrency = (value: number) => Math.round(value * 100) / 100; + +export const formatDeltaPercent = (current: number, previous: number) => { + const roundedCurrent = roundCurrency(current); + const roundedPrevious = roundCurrency(previous); + if (roundedPrevious <= 0) return roundedCurrent > 0 ? '+100.0%' : '0.0%'; + const delta = (roundedCurrent - roundedPrevious) / roundedPrevious; + return `${delta >= 0 ? '+' : ''}${(delta * 100).toFixed(1)}%`; +}; + +export const createPriceDraft = (rule?: ModelPriceRule): PriceDraft => ({ + input: rule ? String(rule.base.input) : '', + output: rule ? String(rule.base.output) : '', + cacheRead: rule ? String(rule.base.cacheRead) : '', + cacheWrite: rule ? String(rule.base.cacheWrite) : '', + tiers: rule?.tiers?.map((tier) => ({ + contextSize: String(tier.contextSize), + input: String(tier.input), + output: String(tier.output), + cacheRead: String(tier.cacheRead), + cacheWrite: String(tier.cacheWrite), + })) ?? [], +}); + +export const parsePriceValue = (value: string) => { + const parsed = Number.parseFloat(value); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0; +}; + +export const parsePriceContextSize = (value: string) => { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0; +}; + +export const formatModelPriceRate = (value: number | undefined) => { + const normalized = Number(value) || 0; + return `$${normalized.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}`; +}; + +export const MODEL_PRICE_SYNC_RATE_FIELDS = [ + ['input', 'usage_stats.model_price_input'], + ['output', 'usage_stats.model_price_output'], + ['cacheRead', 'usage_stats.model_price_cache_read'], + ['cacheWrite', 'usage_stats.model_price_cache_write'], +] as const; diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/monitoring.module.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/monitoring.module.scss new file mode 100644 index 0000000..fc222f9 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/monitoring.module.scss @@ -0,0 +1,7 @@ +@use './styles/base'; +@use './styles/analytics'; +@use './styles/accounts'; +@use './styles/management'; +@use './styles/realtime'; +@use './styles/utilities'; +@use './styles/responsive'; diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/monitoringSettings.ts b/cliproxyapi-pro-management/overlay/src/features/monitoring/monitoringSettings.ts new file mode 100644 index 0000000..9e808a1 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/monitoringSettings.ts @@ -0,0 +1,69 @@ +export type MonitoringSettings = { + retentionDays: number; + webdav: { + enabled: boolean; + intervalMinutes: number; + retentionDays: number; + url: string; + username: string; + password: string; + }; + modelPriceSync: { + enabled: boolean; + intervalMinutes: number; + }; +}; + +export type MonitoringSettingsDraft = { + retentionDays: string; + webdavEnabled: boolean; + webdavIntervalMinutes: string; + webdavRetentionDays: string; + webdavUrl: string; + webdavUsername: string; + webdavPassword: string; + modelPriceSyncEnabled: boolean; + modelPriceSyncIntervalMinutes: string; +}; + +export const createMonitoringSettingsDraft = ( + settings?: MonitoringSettings +): MonitoringSettingsDraft => ({ + retentionDays: String(settings?.retentionDays ?? 0), + webdavEnabled: settings?.webdav.enabled ?? false, + webdavIntervalMinutes: String(settings?.webdav.intervalMinutes ?? 1440), + webdavRetentionDays: String(settings?.webdav.retentionDays ?? 0), + webdavUrl: settings?.webdav.url ?? '', + webdavUsername: settings?.webdav.username ?? '', + webdavPassword: settings?.webdav.password ?? '', + modelPriceSyncEnabled: settings?.modelPriceSync?.enabled ?? false, + modelPriceSyncIntervalMinutes: String(settings?.modelPriceSync?.intervalMinutes ?? 1440), +}); + +const parseNonNegativeInteger = (value: string) => { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0; +}; + +const parsePositiveInteger = (value: string, fallback: number) => { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +}; + +export const buildMonitoringSettingsFromDraft = ( + draft: MonitoringSettingsDraft +): MonitoringSettings => ({ + retentionDays: parseNonNegativeInteger(draft.retentionDays), + webdav: { + enabled: draft.webdavEnabled, + intervalMinutes: parsePositiveInteger(draft.webdavIntervalMinutes, 1440), + retentionDays: parseNonNegativeInteger(draft.webdavRetentionDays), + url: draft.webdavUrl.trim(), + username: draft.webdavUsername.trim(), + password: draft.webdavPassword, + }, + modelPriceSync: { + enabled: draft.modelPriceSyncEnabled, + intervalMinutes: parsePositiveInteger(draft.modelPriceSyncIntervalMinutes, 1440), + }, +}); diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_accounts.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_accounts.scss new file mode 100644 index 0000000..cbb2660 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_accounts.scss @@ -0,0 +1,903 @@ +.accountStatsCard { + display: grid; + gap: 14px; + min-width: 0; + padding: 18px; + border: 1px solid var(--monitor-line); + border-radius: 12px; + background: var(--monitor-surface); + box-shadow: var(--shadow); + overflow: visible; +} + +.accountStatsToolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; +} + +.accountStatsFilters { + display: flex; + align-items: center; + gap: 8px; + flex: 1 1 auto; + min-width: 0; + flex-wrap: wrap; +} + +.accountStatsFilters :global(.form-group) { + margin: 0; +} + +.accountStatsSearchInput { + width: 180px; + min-width: 0; + height: 34px; + min-height: 34px; + padding-block: 0; + padding-right: 30px; + border-radius: 8px; + font-size: 12px; + line-height: 1; +} + +.accountStatsSelect { + height: 34px; + min-height: 34px; + padding: 0 8px; + border: 1px solid var(--monitor-line); + border-radius: 8px; + background: var(--monitor-surface); + color: var(--monitor-ink); + font-size: 12px; + font-weight: 600; + cursor: pointer; + appearance: auto; +} + +.accountStatsSelect:hover { + border-color: color-mix(in srgb, var(--monitor-accent) 35%, var(--monitor-line-strong)); +} + +.accountStatsClearButton { + display: inline-flex; + align-items: center; + justify-content: center; + height: 34px; + min-height: 34px; + padding: 0 10px; + border: 1px solid var(--monitor-line); + border-radius: 8px; + background: var(--monitor-surface); + color: var(--monitor-muted); + font-size: 12px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: + border-color 0.18s ease, + background-color 0.18s ease, + color 0.18s ease; +} + +.accountStatsClearButton:hover { + border-color: color-mix(in srgb, var(--monitor-accent) 35%, var(--monitor-line-strong)); + background: var(--monitor-surface-hover); + color: var(--monitor-ink); +} + +.accountOverviewCardGrid { + --account-card-min-width: 330px; + + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--account-card-min-width)), 1fr)); + align-items: start; + justify-content: start; + gap: 16px; +} + +.accountOverviewCard { + display: grid; + gap: 14px; + align-self: start; + width: 100%; + height: auto; + min-width: 0; + padding: 16px; + border: 1px solid color-mix(in srgb, var(--monitor-line-strong) 58%, var(--monitor-line)); + border-radius: 16px; + background: color-mix(in srgb, var(--floating-surface, #fffdf9) 34%, var(--monitor-surface)); + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(--floating-surface, #fffdf9) 48%, transparent), + 0 8px 18px -18px rgba(45, 42, 38, 0.34); +} + +.accountOverviewCardExpanded { + border-color: color-mix(in srgb, var(--monitor-accent) 28%, var(--monitor-line-strong)); + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(--floating-surface, #fffdf9) 52%, transparent), + 0 10px 20px -18px rgba(45, 42, 38, 0.38), + 0 0 0 1px color-mix(in srgb, var(--monitor-accent) 8%, transparent); +} + +.accountOverviewCardFocused { + border-color: color-mix(in srgb, var(--monitor-accent) 42%, var(--monitor-line)); + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--monitor-accent) 18%, transparent), + 0 12px 24px -18px rgba(45, 42, 38, 0.42), + var(--shadow-lg); +} + +.accountOverviewCardHeader { + display: grid; + gap: 8px; + min-width: 0; +} + +.accountTitleRow { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + gap: 12px; + min-width: 0; +} + +.accountExpandGlyph { + display: inline-flex; + width: 18px; + height: 18px; + align-items: center; + justify-content: center; + color: var(--monitor-muted); + align-self: center; +} + +.accountIdentityLine { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.accountStatusDot { + width: 8px; + height: 8px; + border-radius: 999px; + background: currentColor; + flex: 0 0 auto; +} + +.accountStatusDotEnabled { + color: var(--monitor-green); +} + +.accountStatusDotDisabled, +.accountStatusDotUnavailable { + color: var(--monitor-red); +} + +.accountStatusDotMixed { + color: var(--monitor-amber); +} + +.accountHealthBadge { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + min-height: 26px; + padding: 0 10px; + border: 1px solid transparent; + border-radius: 999px; + font-size: 12px; + font-weight: 800; + white-space: nowrap; +} + +.accountHealthBadgegood { + color: var(--monitor-green); + border-color: color-mix(in srgb, var(--monitor-green) 26%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-green) 11%, var(--monitor-surface)); +} + +.accountHealthBadgewarn { + color: color-mix(in srgb, var(--monitor-amber) 86%, #78350f); + border-color: color-mix(in srgb, var(--monitor-amber) 30%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-amber) 15%, var(--monitor-surface)); +} + +.accountHealthBadgebad { + color: var(--monitor-red); + border-color: color-mix(in srgb, var(--monitor-red) 26%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-red) 11%, var(--monitor-surface)); +} + +.accountOverviewCardTimestamp { + min-width: 0; + overflow: hidden; + color: var(--monitor-muted); + font-size: 12px; + line-height: 1.45; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accountMetaRow { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + min-width: 0; +} + +.accountMetaRow .inlineActionButton { + margin-left: auto; +} + +.accountFocusButton { + min-height: 26px; + padding: 0 8px; + gap: 5px; + border-color: color-mix(in srgb, var(--monitor-line) 56%, transparent); + background: color-mix(in srgb, var(--monitor-surface-elevated) 58%, transparent); + color: var(--monitor-muted); + font-size: 11px; + font-weight: 700; +} + +.accountMetaSeparator { + color: var(--monitor-muted); +} + +.accountOverviewStatusSection, +.accountTokenStructurePanel { + display: grid; + gap: 12px; + min-width: 0; + padding: 16px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); + border-radius: 12px; + background: color-mix(in srgb, var(--monitor-surface-muted) 34%, var(--monitor-surface)); +} + +.accountScopeText { + color: var(--monitor-muted); + font-size: 11px; + line-height: 1.35; +} + +.accountTokenPanel { + display: grid; + gap: 12px; + min-width: 0; +} + +.accountSectionInfo { + display: inline-flex; + width: 16px; + height: 16px; + align-items: center; + justify-content: center; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--monitor-muted) 30%, transparent); + color: var(--monitor-muted); + font-size: 10px; + font-weight: 800; +} + +.healthMetricGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.healthMetricItem { + display: grid; + gap: 5px; + min-width: 0; +} + +.healthMetricItem span { + min-width: 0; + overflow: hidden; + color: var(--monitor-muted); + font-size: 11px; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.healthMetricItem strong { + min-width: 0; + overflow: hidden; + color: var(--monitor-ink); + font-size: 16px; + font-weight: 750; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.healthMetricItem .successFailureValue, +.healthMetricItem .successFailureValue span { + font-size: inherit; + font-weight: inherit; + line-height: inherit; +} + +.monitoringStatusBar { + display: flex; + align-items: center; + gap: 8px; +} + +.monitoringStatusBlocks { + display: flex; + gap: 3px; + flex: 1; + min-width: 0; + position: relative; +} + +.monitoringStatusBlockWrapper { + flex: 1; + min-width: 6px; + position: relative; +} + +.monitoringStatusBlockButton { + width: 100%; + padding: 3px 0; + display: flex; + align-items: center; + background: transparent; + border: 0; + border-radius: 999px; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.monitoringStatusBlockButton:focus-visible { + outline: 2px solid color-mix(in srgb, var(--monitor-accent) 78%, white); + outline-offset: 2px; +} + +.monitoringStatusBlockActive .monitoringStatusBlock, +.monitoringStatusBlockButton:hover .monitoringStatusBlock, +.monitoringStatusBlockButton:focus-visible .monitoringStatusBlock { + transform: scaleY(1.35); + opacity: 1; +} + +.monitoringStatusBlock { + width: 100%; + height: 7px; + border-radius: 999px; + opacity: 0.92; + transform-origin: center; + transition: + transform 0.15s ease, + opacity 0.15s ease; +} + +.monitoringStatusBlockIdle { + background-color: color-mix(in srgb, var(--monitor-line) 82%, transparent); +} + +.monitoringStatusTooltip { + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + z-index: 20; + padding: 6px 10px; + border: 1px solid var(--monitor-line); + border-radius: 8px; + background: var(--monitor-surface); + box-shadow: var(--shadow-lg); + color: var(--monitor-ink); + font-size: 11px; + line-height: 1.5; + white-space: nowrap; + pointer-events: none; +} + +.monitoringStatusTooltip::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 5px solid transparent; + border-top-color: var(--monitor-surface); +} + +.monitoringStatusTooltip::before { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 6px solid transparent; + border-top-color: var(--monitor-line); +} + +.monitoringStatusTooltipLeft { + left: 0; + transform: translateX(0); +} + +.monitoringStatusTooltipLeft::after, +.monitoringStatusTooltipLeft::before { + left: 8px; + transform: none; +} + +.monitoringStatusTooltipRight { + left: auto; + right: 0; + transform: translateX(0); +} + +.monitoringStatusTooltipRight::after, +.monitoringStatusTooltipRight::before { + left: auto; + right: 8px; + transform: none; +} + +.monitoringTooltipTime { + display: block; + margin-bottom: 2px; + color: var(--monitor-muted); +} + +.monitoringTooltipStats { + display: flex; + align-items: center; + gap: 8px; +} + +.monitoringTooltipSuccess { + color: var(--monitor-green); +} + +.monitoringTooltipFailure { + color: var(--monitor-red); +} + +.monitoringTooltipRate { + color: var(--monitor-muted); +} + +.monitoringStatusRate { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 0 10px; + border-radius: 999px; + background: color-mix(in srgb, var(--monitor-surface-elevated) 88%, var(--monitor-surface)); + color: var(--monitor-muted); + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.monitoringStatusRatePlaceholder { + display: none; +} + +.monitoringStatusRateHigh { + color: var(--monitor-green); + background: color-mix(in srgb, var(--monitor-green) 14%, var(--monitor-surface)); +} + +.monitoringStatusRateMedium { + color: color-mix(in srgb, var(--monitor-amber) 86%, #78350f); + background: color-mix(in srgb, var(--monitor-amber) 16%, var(--monitor-surface)); +} + +.monitoringStatusRateLow { + color: var(--monitor-red); + background: color-mix(in srgb, var(--monitor-red) 14%, var(--monitor-surface)); +} + +.accountOverviewMetricGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + align-items: start; +} + +.accountOverviewMetricCard { + display: flex; + flex-direction: column; + justify-content: center; + gap: 5px; + min-height: 68px; + min-width: 0; + padding: 12px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 80%, transparent); + border-radius: 10px; + background: color-mix(in srgb, var(--monitor-surface-elevated) 80%, var(--monitor-surface)); +} + +.accountOverviewMetricLabel { + display: inline-flex; + align-items: center; + gap: 4px; + min-width: 0; + color: var(--monitor-muted); + font-size: 10px; + font-weight: 700; + line-height: 1.3; + white-space: nowrap; +} + +.accountMetricIcon { + display: inline-flex; + width: 14px; + height: 14px; + align-items: center; + justify-content: center; + flex: 0 0 auto; + border-radius: 6px; + background: color-mix(in srgb, var(--monitor-accent) 11%, var(--monitor-surface)); + color: var(--monitor-accent-strong); +} + +.accountMetricIcon::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 999px; + background: currentColor; +} + +.accountMetricIconTotal { + background: color-mix(in srgb, var(--monitor-accent) 13%, var(--monitor-surface)); + color: var(--monitor-accent-strong); +} + +.accountMetricIconInput { + background: color-mix(in srgb, var(--monitor-green) 13%, var(--monitor-surface)); + color: var(--monitor-green); +} + +.accountMetricIconOutput { + background: color-mix(in srgb, var(--monitor-amber) 17%, var(--monitor-surface)); + color: var(--monitor-amber); +} + +.accountMetricIconCached { + background: color-mix(in srgb, var(--monitor-cyan) 14%, var(--monitor-surface)); + color: var(--monitor-cyan); +} + +.accountOverviewMetricValue { + min-width: 0; + font-size: 16px; + font-weight: 750; + line-height: 1.35; + white-space: nowrap; + overflow-wrap: normal; + word-break: normal; +} + +.accountOverviewCardBody { + display: grid; + gap: 14px; + padding-top: 2px; + min-width: 0; +} + +.accountModelListPanel { + display: grid; + align-content: start; + gap: 10px; + min-width: 0; +} + +.accountModelViewAllButton { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0; + border: 0; + background: transparent; + color: var(--monitor-accent-strong); + font: inherit; + font-size: 12px; + font-weight: 700; + cursor: pointer; +} + +.accountModelViewAllButton:hover { + transform: none; + color: var(--monitor-accent); +} + +.accountModelList { + display: grid; + min-width: 0; +} + +.accountModelItem { + display: grid; + gap: 6px; + min-width: 0; + padding: 10px 0; + border-top: 1px solid color-mix(in srgb, var(--monitor-line) 84%, transparent); +} + +.accountModelItem:first-child { + border-top: 0; +} + +.accountModelRow { + display: grid; + align-items: start; + gap: 6px; + width: 100%; + min-width: 0; + padding: 0; + border: 0; + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} + +.accountModelRow:hover { + transform: none; + color: var(--monitor-accent-strong); +} + +.accountModelStat { + display: inline-flex; + align-items: baseline; + gap: 3px; + min-width: 0; + text-align: left; +} + +.accountModelStat small { + color: var(--monitor-muted); + font-size: 11px; + font-weight: 700; + line-height: 1.4; + letter-spacing: 0; + white-space: nowrap; +} + +.accountModelStat strong { + min-width: 0; + overflow: hidden; + color: var(--monitor-ink); + font-size: 11px; + font-weight: 700; + line-height: 1.4; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accountModelMetaLine { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px 8px; + min-width: 0; + color: var(--monitor-muted); +} + +.accountModelMetaLine .accountModelStat { + min-width: 0; +} + +.accountModelChevron { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 14px; + min-width: 14px; + height: 16px; + color: var(--monitor-muted); + line-height: 1; +} + +.accountModelExpanded { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 8px 12px; + padding: 8px 10px 4px; + border-top: 1px dashed color-mix(in srgb, var(--monitor-line) 80%, transparent); +} + +.accountModelExpandedItem { + display: grid; + gap: 2px; + min-width: 0; +} + +.accountModelExpandedItem small { + min-width: 0; + overflow: hidden; + color: var(--monitor-muted); + font-size: 10px; + font-weight: 700; + line-height: 1.2; + letter-spacing: 0; + text-transform: uppercase; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accountModelExpandedItem strong { + min-width: 0; + overflow: hidden; + color: var(--monitor-ink); + font-size: 12px; + font-weight: 700; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accountModelName { + display: inline-block; + min-width: 0; + max-width: 100%; + overflow: hidden; + color: var(--monitor-ink); + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.summaryCard { + display: grid; + gap: 6px; + min-width: 0; + min-height: 104px; + padding: 16px; + border-radius: 8px; + background: var(--monitor-surface); + transition: + transform 0.18s ease, + border-color 0.18s ease, + box-shadow 0.18s ease; +} + +.summaryCard:hover { + transform: translateY(-1px); + border-color: var(--monitor-line-strong); + box-shadow: var(--shadow); +} + +.summaryLabel { + color: var(--monitor-muted); + font-size: 13px; + font-weight: 500; + letter-spacing: 0; + text-transform: none; +} + +.summaryValue { + min-width: 0; + max-width: 100%; + overflow: hidden; + font-size: 22px; + font-weight: 800; + line-height: 1.1; + letter-spacing: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.summaryMeta { + margin-top: 0; + color: var(--monitor-muted); + font-size: 12px; + line-height: 1.5; +} + +.statusBadge { + gap: 6px; + min-height: 24px; + padding: 0 8px; + justify-content: center; + font-size: 12px; + font-weight: 600; + white-space: nowrap; +} + +.tonegood { + color: var(--monitor-green); +} + +.tonewarn { + color: var(--monitor-amber); +} + +.tonebad { + color: var(--monitor-red); +} + +.statusBadge.tonegood { + border-color: color-mix(in srgb, var(--monitor-green) 26%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-green) 10%, var(--monitor-surface)); +} + +.statusBadge.tonewarn { + border-color: color-mix(in srgb, var(--monitor-amber) 24%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-amber) 10%, var(--monitor-surface)); +} + +.statusBadge.tonebad { + border-color: color-mix(in srgb, var(--monitor-red) 26%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-red) 10%, var(--monitor-surface)); +} + +.successFailureValue { + display: inline-flex; + align-items: baseline; + gap: 0.12em; + min-width: 0; +} + +.goodText { + color: var(--monitor-green); +} + +.warnText { + color: var(--monitor-amber); +} + +.badText { + color: var(--monitor-red); +} + +.patternBars { + display: inline-grid; + grid-auto-flow: column; + grid-auto-columns: 6px; + gap: 4px; + align-items: center; + padding: 7px 10px; + border-radius: 999px; + border: 1px solid var(--monitor-line); + background: var(--monitor-surface-muted); +} + +.patternBarsPlain { + grid-auto-columns: 4px; + gap: 2px; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; +} + +.patternBar { + width: 6px; + height: 16px; + border-radius: 999px; + background: color-mix(in srgb, var(--monitor-muted) 24%, transparent); +} + +.patternBarPlain { + width: 4px; + height: 24px; +} + +.patternSuccess { + background: var(--monitor-green); +} + +.patternFailed { + background: var(--monitor-red); +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_analytics.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_analytics.scss new file mode 100644 index 0000000..51b98c5 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_analytics.scss @@ -0,0 +1,1245 @@ +.usageStatsHero { + display: grid; + gap: 0; + min-width: 0; + padding-top: 2px; +} + +.usageStatsGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.usageStatsCard { + display: grid; + gap: 13px; + min-width: 0; + padding: 15px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 86%, transparent); + border-radius: 14px; + background: + radial-gradient(circle at 90% 10%, color-mix(in srgb, var(--usage-accent, var(--monitor-accent)) 10%, transparent), transparent 34%), + color-mix(in srgb, var(--monitor-surface) 96%, var(--monitor-bg)); + box-shadow: 0 14px 34px -30px rgba(15, 23, 42, 0.72); +} + +.usageStatsCard:nth-child(1) { + --usage-accent: #2563eb; +} + +.usageStatsCard:nth-child(2) { + --usage-accent: #7c3aed; +} + +.usageStatsCard:nth-child(3) { + --usage-accent: #16a34a; +} + +.usageStatsCard:nth-child(4) { + --usage-accent: #d97706; +} + +.usageStatsCard:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--usage-accent) 30%, var(--monitor-line)); +} + +.usageStatsCard { + transition: + transform 0.18s ease, + border-color 0.18s ease, + box-shadow 0.18s ease; +} + +.usageStatsCard:hover { + box-shadow: 0 16px 34px -28px var(--usage-accent); +} + +.usageStatsCardHeader { + display: flex; + align-items: center; + gap: 10px; + color: var(--monitor-ink); + font-size: 15px; + font-weight: 700; +} + +.usageStatsIcon { + width: 30px; + height: 30px; + border-radius: 10px; + background: var(--usage-accent-soft); + color: var(--usage-accent); + position: relative; + flex: 0 0 auto; +} + +.usageStatsIcon::before, +.usageStatsIcon::after { + content: ''; + position: absolute; + border-radius: 999px; + background: currentColor; +} + +.usageStatsIcon::before { + left: 8px; + right: 8px; + bottom: 7px; + height: 4px; +} + +.usageStatsIcon::after { + width: 5px; + height: 12px; + right: 9px; + bottom: 10px; + box-shadow: -7px 4px 0 currentColor; +} + +.usageStatsIconblue { + --usage-accent: #2563eb; + --usage-accent-soft: rgba(37, 99, 235, 0.12); +} + +.usageStatsIconpurple { + --usage-accent: #7c3aed; + --usage-accent-soft: rgba(124, 58, 237, 0.12); +} + +.usageStatsIcongreen { + --usage-accent: #16a34a; + --usage-accent-soft: rgba(22, 163, 74, 0.12); +} + +.usageStatsIconamber { + --usage-accent: #d97706; + --usage-accent-soft: rgba(217, 119, 6, 0.12); +} + +.usageStatsBody { + display: grid; + gap: 8px; +} + +.usageStatsBody span, +.usageStatsFooter span { + color: var(--monitor-muted); + font-size: 12px; + line-height: 1.35; +} + +.usageStatsBody strong { + min-width: 0; + overflow: hidden; + color: var(--monitor-ink); + font-size: 24px; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.usageStatsFooter { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + padding-top: 12px; + border-top: 1px solid var(--monitor-line); +} + +.usageStatsFooter div { + display: grid; + gap: 4px; + min-width: 0; +} + +.usageStatsFooter strong { + min-width: 0; + overflow: hidden; + color: var(--monitor-ink); + font-size: 13px; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.usageStatsCardTokens .usageStatsFooter { + grid-template-columns: minmax(0, 0.68fr) minmax(0, 1.32fr); + column-gap: 6px; +} + +.usageTrendSection { + display: grid; + gap: 16px; + min-width: 0; + overflow: visible; + position: relative; + z-index: 3; +} + +.usageTrendHeader, +.usageTrendCollapsed { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 18px; + min-width: 0; +} + +.usageTrendCopy { + display: grid; + gap: 8px; + min-width: 0; +} + +.usageTrendCopy h2, +.usageTrendCollapsed h2 { + margin: 0; + color: var(--monitor-ink); + font-size: 22px; + font-weight: 800; + letter-spacing: -0.03em; +} + +.usageTrendCopy p, +.usageTrendCollapsed p { + margin: 0; + color: var(--monitor-muted); + font-size: 13px; + line-height: 1.55; +} + +.usageTrendActions { + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-end; + gap: 8px; + max-width: 100%; + min-width: 0; +} + +.timeRangeControl, +.usageTrendRangeControl { + display: flex; + flex-wrap: nowrap; + max-width: 100%; + overflow-x: auto; + scrollbar-width: none; +} + +.timeRangeControl::-webkit-scrollbar, +.usageTrendRangeControl::-webkit-scrollbar { + display: none; +} + +.timeRangeButton, +.usageTrendRangeButton { + flex: 0 0 auto; + min-width: 50px; + padding: 0 12px; +} + +.usageTrendHideButton { + flex: 0 0 auto; + min-height: 36px; + padding: 0 14px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 80%, transparent); + background: color-mix(in srgb, var(--monitor-surface-muted) 78%, var(--monitor-surface)); +} + +.usageTrendHideButton:hover { + color: var(--monitor-ink); +} + +.mobileHeaderHideButton { + display: none; +} + +.usageTrendCollapsed { + padding: 18px 20px; + border: 1px dashed color-mix(in srgb, var(--monitor-line) 82%, transparent); + border-radius: 14px; + background: color-mix(in srgb, var(--monitor-surface) 72%, transparent); +} + +.usageTrendInsightsGrid { + display: grid; + grid-template-columns: minmax(0, 1.22fr) minmax(360px, 0.78fr); + gap: 16px; + align-items: stretch; + --ranking-card-height: 500px; +} + +.usageTrendChartCard { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 16px; + min-width: 0; + height: 500px; + padding: 20px; + border: 1px solid var(--monitor-line); + border-radius: 12px; + background: var(--monitor-surface); + box-shadow: var(--shadow); + overflow: hidden; +} + +.tokenDistributionCard { + overflow: visible; +} + +.usageTrendLineCard, +.tokenDistributionCard { + background: var(--monitor-surface); +} + +.trendCardHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + min-width: 0; +} + +.trendCardHeader > div:first-child { + min-width: 0; +} + +.trendCardHeader h3 { + margin: 0; + color: var(--monitor-ink); + font-size: 18px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.trendCardHeader p { + margin: 6px 0 0; + color: var(--monitor-muted); + font-size: 13px; + line-height: 1.5; +} + +.usageTrendApiKeySelect { + width: 150px; + flex: 0 0 auto; +} + +.tokenDistributionHeader { + align-items: flex-start; +} + +.tokenCostBadge { + display: grid; + gap: 3px; + flex: 0 0 auto; + min-width: 108px; + padding: 8px 10px; + border: 1px solid color-mix(in srgb, #047857 26%, var(--monitor-line)); + border-radius: 10px; + background: color-mix(in srgb, #047857 8%, var(--monitor-surface)); + text-align: right; +} + +.tokenCostBadge span { + color: var(--monitor-muted); + font-size: 11px; + font-weight: 700; + line-height: 1.1; +} + +.tokenCostBadge strong { + color: #047857; + font-size: 14px; + font-weight: 850; + letter-spacing: -0.01em; + line-height: 1.15; +} + +.trendHeaderStats { + display: grid; + grid-template-columns: repeat(3, minmax(64px, auto)); + gap: 8px; + flex: 0 0 auto; +} + +.trendHeaderStats div { + display: grid; + gap: 3px; + min-width: 0; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); + background: color-mix(in srgb, var(--monitor-surface-muted) 48%, var(--monitor-surface)); +} + +.trendHeaderStats span { + color: var(--monitor-muted); + font-size: 11px; + font-weight: 700; + line-height: 1.1; +} + +.trendHeaderStats strong { + color: var(--monitor-ink); + font-size: 13px; + font-weight: 800; + letter-spacing: -0.01em; + line-height: 1.15; +} + +.professionalChartShell { + display: grid; + grid-template-rows: auto minmax(250px, 260px) auto; + gap: 16px; + min-width: 0; + min-height: 0; + align-content: start; +} + +.trendChartLegend { + display: inline-flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 8px 18px; + min-width: 0; + color: var(--monitor-ink); +} + +.trendChartLegend span { + display: inline-flex; + align-items: center; + gap: 7px; + color: color-mix(in srgb, var(--monitor-ink) 64%, var(--monitor-muted)); + font-size: 13px; + font-weight: 750; + letter-spacing: -0.01em; + line-height: 1; + white-space: nowrap; +} + +.trendChartLegend span::before { + content: ''; + width: 22px; + height: 3px; + border-radius: 999px; + background: var(--series-color); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--series-color) 12%, transparent); +} + +.trendSummaryStrip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.trendSummaryItem { + position: relative; + display: grid; + gap: 6px; + min-width: 0; + padding: 12px 12px 12px 18px; + overflow: hidden; + border-radius: 14px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 84%, transparent); + background: color-mix(in srgb, var(--monitor-surface) 96%, var(--monitor-bg)); + box-shadow: 0 10px 24px -26px rgba(15, 23, 42, 0.58); +} + +.trendSummaryItem::before { + content: ''; + position: absolute; + inset: 10px auto 10px 0; + width: 4px; + border-radius: 999px; + background: var(--series-color); +} + +.trendSummaryItem span { + min-width: 0; + overflow: hidden; + color: var(--monitor-muted); + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.trendSummaryItem strong { + min-width: 0; + overflow: hidden; + color: var(--monitor-ink); + font-size: 18px; + font-weight: 850; + letter-spacing: -0.03em; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.usageTrendSvg { + display: block; + width: 100%; + height: 260px; + min-height: 0; + overflow: visible; +} + +.chartGridLine { + stroke: color-mix(in srgb, var(--monitor-line) 82%, transparent); + stroke-dasharray: 4 7; + stroke-width: 1; +} + +.chartAxisBase, +.chartYAxisRequests, +.chartYAxisCost, +.chartYAxisTokens, +.chartXAxisTick { + shape-rendering: crispEdges; +} + +.chartAxisBase { + stroke: color-mix(in srgb, var(--monitor-ink) 52%, transparent); + stroke-width: 1.1; +} + +.chartYAxisRequests, +.chartXAxisTick { + stroke: #2563eb; + stroke-width: 1.1; +} + +.chartYAxisCost { + stroke: #047857; + stroke-width: 1.1; +} + +.chartYAxisTokens { + stroke: #7c3aed; + stroke-width: 1.1; +} + +.chartAxisLabel, +.chartXAxisLabel { + font-size: 13px; + font-weight: 650; + dominant-baseline: middle; +} + +.chartAxisLabel { + text-anchor: end; +} + +.chartAxisLabelRequests { + fill: #2563eb; +} + +.chartAxisLabelCost { + fill: #047857; + text-anchor: end; +} + +.chartAxisLabelTokens { + fill: #7c3aed; + text-anchor: end; +} + +.chartXAxisLabel { + fill: color-mix(in srgb, var(--monitor-ink) 66%, var(--monitor-muted)); + text-anchor: middle; +} + +.trendAreaFill { + opacity: 0.92; +} + +.trendSeriesLine { + fill: none; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.8; +} + +.trendSeriesDot { + fill: var(--monitor-surface); + stroke-width: 2.4; + transition: r 0.15s ease; +} + +.trendHoverTarget { + outline: none; +} + +.trendHoverGuide { + stroke: color-mix(in srgb, var(--monitor-muted) 35%, transparent); + stroke-width: 1; +} + +.trendTooltipLayer rect { + fill: color-mix(in srgb, var(--monitor-surface) 98%, var(--monitor-bg)); + stroke: color-mix(in srgb, var(--monitor-line-strong) 68%, transparent); + stroke-width: 1; + filter: drop-shadow(0 14px 26px rgba(15, 23, 42, 0.18)); +} + +.trendTooltipLayer text { + font-size: 13px; + font-weight: 700; +} + +.trendTooltipTitle { + fill: var(--monitor-ink); + font-size: 14px !important; + font-weight: 800 !important; +} + +.trendTooltipMetric { + font-size: 13px !important; +} + +.tokenStatCardList { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-content: stretch; + grid-auto-rows: minmax(78px, 1fr); + gap: 10px; + min-width: 0; + min-height: 0; + overflow: visible; +} + +.tokenStatCard { + --token-stat-accent: #6366f1; + --token-stat-border: color-mix(in srgb, var(--token-stat-accent) 32%, var(--monitor-line)); + --token-stat-bg: color-mix(in srgb, var(--token-stat-accent) 8%, var(--monitor-surface)); + + display: grid; + align-content: space-between; + gap: 7px; + min-width: 0; + min-height: 0; + padding: 12px 14px; + border: 1px solid var(--token-stat-border); + border-radius: 12px; + background: var(--token-stat-bg); +} + +.tokenStatCardPurple { + --token-stat-accent: #6d5dfc; +} + +.tokenStatCardBlue { + --token-stat-accent: #3b82f6; +} + +.tokenStatCardCyan { + --token-stat-accent: #06b6d4; +} + +.tokenStatCardGreen { + --token-stat-accent: #22c55e; +} + +.tokenStatCardAmber { + --token-stat-accent: #f59e0b; +} + +.tokenStatCardRose { + --token-stat-accent: #fb7185; +} + +.tokenStatCardIndigo { + --token-stat-accent: #8b5cf6; +} + +.tokenStatCardSlate { + --token-stat-accent: #64748b; +} + +.tokenStatCardHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; +} + +.tokenStatCardHeader span, +.tokenStatCardHeader strong { + color: color-mix(in srgb, var(--monitor-ink) 78%, var(--monitor-muted)); + font-size: 13px; + font-weight: 800; + letter-spacing: -0.01em; +} + +.tokenStatCardHeader strong { + flex: 0 0 auto; + color: var(--monitor-muted); + font-size: 12px; + letter-spacing: 0.01em; +} + +.tokenStatCardValue { + color: var(--monitor-ink); + font-size: clamp(21px, 2.5vw, 28px); + font-weight: 800; + line-height: 1; + letter-spacing: -0.04em; +} + +.tokenStatProgressTrack { + height: 4px; + overflow: hidden; + border-radius: 999px; + background: color-mix(in srgb, var(--monitor-line) 58%, transparent); +} + +.tokenStatProgressTrack span { + display: block; + width: var(--token-stat-width, 0%); + height: 100%; + border-radius: inherit; + background: var(--token-stat-accent); +} + +.rankingGrid { + display: grid; + grid-template-columns: minmax(0, 1.22fr) minmax(360px, 0.78fr); + gap: 16px; + align-items: stretch; + --ranking-card-height: 500px; +} + +.modelStatsCard, +.apiKeyRankingCard { + min-width: 0; + padding: 20px; + border: 1px solid var(--monitor-line); + border-radius: 12px; + background: var(--monitor-surface); + box-shadow: var(--shadow); +} + +.modelStatsCard, +.apiKeyRankingCard { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + height: var(--ranking-card-height); +} + +.modelStatsCard { + overflow: visible; +} + +.apiKeyRankingCard { + overflow: hidden; +} + +.modelStatsCard { + position: relative; + z-index: 2; + --donut-color-0: #2563eb; + --donut-color-1: #22c55e; + --donut-color-2: #f97316; + --donut-color-3: #8b5cf6; + --donut-color-4: #06b6d4; +} + +.rankingHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 18px; + min-width: 0; +} + +.rankingHeader h3 { + margin: 0; + font-size: 18px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.rankingHeader > .rankingMetricSwitch { + flex-shrink: 0; + align-self: flex-start; +} + +.rankingMetricSwitch { + display: inline-flex; + flex: 0 0 auto; + gap: 3px; + padding: 3px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 80%, transparent); + background: color-mix(in srgb, var(--monitor-surface-muted) 78%, var(--monitor-surface)); +} + +.rankingMetricButton { + min-height: 28px; + padding: 0 10px; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--monitor-muted); + font-size: 12px; + font-weight: 700; + cursor: pointer; + transition: + background-color 0.18s ease, + color 0.18s ease, + box-shadow 0.18s ease; +} + +.rankingMetricButton:hover:not(:disabled) { + color: var(--monitor-ink); +} + +.rankingMetricButton:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.rankingMetricButtonActive { + background: var(--monitor-accent); + color: var(--primary-contrast, #fff); + box-shadow: 0 8px 18px -14px rgba(0, 0, 0, 0.45); +} + +.rankingHeader p { + margin: 6px 0 0; + color: var(--monitor-muted); + font-size: 13px; + line-height: 1.5; +} + +.modelStatsLayout { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(220px, 260px); + gap: 16px; + align-items: stretch; + min-height: 0; + overflow: visible; +} + +.modelStatsLayout > * { + min-height: 0; + max-height: 100%; +} + +.modelStatsList, +.apiKeyRankingScroll { + display: grid; + align-content: start; + grid-auto-rows: max-content; + gap: 10px; +} + +.modelStatsList, +.apiKeyRankingScroll { + min-height: 0; + max-height: 100%; + overflow: auto; + overscroll-behavior: contain; + padding-right: 4px; +} + +.apiKeyRankingList { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 12px; + min-height: 0; +} + +.modelStatsRow, +.apiKeyRankingRow { + min-width: 0; + padding: 12px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); + background: color-mix(in srgb, var(--monitor-surface-elevated) 82%, var(--monitor-surface)); +} + +.modelStatsRow { + display: grid; + grid-template-columns: 1fr; + align-items: center; + gap: 12px; +} + +.modelStatsRow, +.apiKeyRankingRow { + transition: + transform 0.18s ease, + border-color 0.18s ease, + background-color 0.18s ease; +} + +.modelStatsRow:hover, +.apiKeyRankingRow:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--monitor-accent) 28%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-accent) 5%, var(--monitor-surface-elevated)); +} + +.rankingIndex { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + flex: 0 0 auto; + border-radius: 999px; + background: var(--monitor-accent-soft); + color: var(--monitor-accent-strong); + font-size: 12px; + font-weight: 800; +} + +.modelStatsMain, +.apiKeyRankingRow { + display: grid; + gap: 9px; + min-width: 0; +} + +.modelStatsTitleLine, +.apiKeyRankingTopLine, +.apiKeyRankingName, +.modelStatsMetaLine, +.apiKeyRankingMetaLine { + display: flex; + align-items: center; + min-width: 0; +} + +.modelStatsTitleLine, +.apiKeyRankingTopLine { + justify-content: space-between; + gap: 12px; +} + +.modelStatsTitleLine strong, +.apiKeyRankingName strong { + min-width: 0; + overflow: hidden; + color: var(--monitor-ink); + font-size: 14px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.modelStatsTitleLine span, +.apiKeyRankingTopLine > span { + flex: 0 0 auto; + color: var(--monitor-ink); + font-size: 13px; + font-weight: 800; +} + +.modelStatsTitleLine span { + padding: 3px 8px; + border-radius: 999px; + background: var(--monitor-accent-soft); + color: var(--monitor-accent-strong); +} + +.modelStatsMetaLine, +.apiKeyRankingMetaLine { + flex-wrap: wrap; + gap: 8px 12px; + color: var(--monitor-muted); + font-size: 12px; +} + +.modelStatsBar, +.apiKeyRankingBar { + position: relative; + overflow: hidden; + height: 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--monitor-line) 70%, transparent); +} + +.modelStatsBar::before, +.apiKeyRankingBar::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.28), transparent); + opacity: 0.35; +} + +.modelStatsBar::after, +.apiKeyRankingBar::after { + content: ''; + position: absolute; + inset: 0 auto 0 0; + width: var(--ranking-width, 0%); + border-radius: inherit; +} + +.modelStatsBar::after { + background: linear-gradient(90deg, var(--monitor-accent), var(--monitor-cyan)); +} + +.apiKeyRankingBar::after { + background: linear-gradient(90deg, #22c55e, #86efac); +} + +.modelSharePanel { + display: grid; + grid-template-rows: auto minmax(0, auto) auto; + align-content: center; + justify-items: center; + gap: 12px; + min-width: 0; + min-height: 0; + max-height: 100%; + padding: 12px; + border-radius: 16px; + border: 1px solid color-mix(in srgb, var(--monitor-accent) 16%, var(--monitor-line)); + background: + radial-gradient(circle at 50% 20%, color-mix(in srgb, var(--monitor-accent) 10%, transparent), transparent 38%), + color-mix(in srgb, var(--monitor-surface-muted) 72%, var(--monitor-surface)); +} + +.modelShareHeader { + display: grid; + gap: 4px; + justify-self: stretch; + text-align: left; +} + +.modelShareHeader strong { + font-size: 15px; + line-height: 1.25; +} + +.modelShareHeader span { + color: var(--monitor-muted); + font-size: 12px; +} + +.donutChart { + position: relative; + display: grid; + place-items: center; + width: min(170px, 100%); + aspect-ratio: 1; + border-radius: 50%; + background: var(--donut-bg); +} + +.donutChart::after { + content: ''; + position: absolute; + inset: 22%; + border-radius: 50%; + background: var(--monitor-surface); + box-shadow: inset 0 0 0 1px var(--monitor-line); +} + +.donutCenter { + position: relative; + z-index: 1; + display: grid; + gap: 5px; + justify-items: center; + max-width: 78%; + text-align: center; +} + +.donutCenter span { + color: var(--monitor-muted); + font-size: 12px; +} + +.donutCenter strong { + max-width: 100%; + overflow: hidden; + font-size: 18px; + font-weight: 800; + text-overflow: ellipsis; + white-space: nowrap; +} + +.donutTooltip { + position: absolute; + right: 50%; + bottom: calc(100% + 12px); + z-index: 5; + width: min(300px, calc(100vw - 48px)); + max-height: 260px; + overflow: auto; + padding: 12px; + border-radius: 12px; + border: 1px solid var(--monitor-line); + background: color-mix(in srgb, var(--monitor-surface-elevated) 96%, var(--monitor-bg)); + box-shadow: 0 18px 42px -26px rgba(15, 23, 42, 0.55); + opacity: 0; + pointer-events: none; + transform: translate(50%, 6px); + transition: + opacity 0.16s ease, + transform 0.16s ease; +} + +.donutChart:hover .donutTooltip, +.donutChart:focus-within .donutTooltip { + opacity: 1; + transform: translate(50%, 0); +} + +.donutTooltip strong { + display: block; + margin-bottom: 8px; + color: var(--monitor-ink); + font-size: 12px; +} + +.donutTooltip div { + display: grid; + gap: 7px; +} + +.donutTooltip span { + display: grid; + grid-template-columns: 8px minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + min-width: 0; + color: var(--monitor-muted); + font-size: 12px; +} + +.donutTooltip i { + width: 8px; + height: 8px; + border-radius: 999px; +} + +.donutTooltip em { + min-width: 0; + overflow: hidden; + font-style: normal; + text-overflow: ellipsis; + white-space: nowrap; +} + +.donutTooltip b { + color: var(--monitor-ink); + font-size: 12px; +} + +.modelSharePanel .donutTooltip { + right: auto; + bottom: auto; + left: calc(100% + 14px); + top: 50%; + transform: translate(0, -50%); +} + +.modelSharePanel .donutChart:hover .donutTooltip, +.modelSharePanel .donutChart:focus-within .donutTooltip { + transform: translate(0, -50%); +} + +.modelSharePanel .modelLegend { + margin-top: 2px; +} + +.modelLegend { + display: grid; + gap: 6px; + width: 100%; + min-width: 0; + min-height: 0; + overflow: visible; +} + +.modelLegendItem { + display: grid; + grid-template-columns: 10px minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + min-width: 0; + color: var(--monitor-muted); + font-size: 12px; +} + +.modelLegendItem span:nth-child(2) { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.modelLegendItem strong { + color: var(--monitor-ink); + font-size: 12px; +} + +.modelLegendDot { + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--legend-color, var(--donut-color-0)); +} + +.apiKeyRankingCard { + display: grid; + align-self: start; + align-content: start; +} + +.apiKeyRankingName { + gap: 10px; +} + +.apiKeyRankingSummary { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px 12px; + align-items: end; + padding: 16px; + border-radius: 14px; + border: 1px solid color-mix(in srgb, #22c55e 24%, var(--monitor-line)); + background: + radial-gradient(circle at 100% 0%, rgba(34, 197, 94, 0.14), transparent 42%), + color-mix(in srgb, #22c55e 7%, var(--monitor-surface)); +} + +.apiKeyRankingSummary span, +.apiKeyRankingSummary small { + color: var(--monitor-muted); + font-size: 12px; +} + +.apiKeyRankingSummary strong { + grid-row: 1 / span 2; + grid-column: 2; + color: var(--monitor-ink); + font-size: 24px; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1; +} + +.apiKeyRankingSummary small { + grid-column: 1; +} + +.apiKeyRankingList { + gap: 14px; +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_base.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_base.scss new file mode 100644 index 0000000..f8000f6 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_base.scss @@ -0,0 +1,720 @@ +.page, +.monitorModal { + --monitor-bg: var(--bg-secondary); + --monitor-surface: var(--bg-primary); + --monitor-surface-elevated: color-mix(in srgb, var(--bg-primary) 88%, var(--bg-secondary)); + --monitor-surface-muted: var(--bg-secondary); + --monitor-surface-hover: var(--bg-tertiary); + --monitor-ink: var(--text-primary); + --monitor-muted: var(--text-secondary); + --monitor-line: var(--border-color); + --monitor-line-strong: var(--border-primary); + --monitor-accent: var(--primary-color); + --monitor-accent-strong: var(--primary-color); + --monitor-accent-soft: rgba(59, 130, 246, 0.12); + --monitor-cyan: #06b6d4; + --monitor-green: var(--success-color); + --monitor-amber: #f59e0b; + --monitor-red: var(--error-color); + color: var(--monitor-ink); +} + +.page { + container: monitoring-page / inline-size; + display: grid; + gap: 24px; + min-width: 0; +} + +.masthead, +.panel { + position: relative; + overflow: hidden; + border: 1px solid var(--monitor-line); + border-radius: 12px; + background: var(--monitor-surface); + box-shadow: var(--shadow); +} + +.masthead { + display: block; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.mastheadGlow { + display: none; +} + +.mastheadCopy { + display: grid; + gap: 14px; + min-width: 0; +} + +.titleRow { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + gap: 20px; + min-width: 0; +} + +.title { + margin: 0; + min-width: 0; + font-size: 28px; + font-weight: 700; + line-height: 1.15; + letter-spacing: 0; +} + +.titleActions { + display: inline-flex; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-end; + gap: 8px; + min-width: 0; + max-width: 100%; + overflow-x: auto; + padding: 4px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); + border-radius: 14px; + background: color-mix(in srgb, var(--monitor-surface) 74%, transparent); + box-shadow: 0 12px 28px -26px rgba(15, 23, 42, 0.5); + scrollbar-width: none; +} + +.titleActions::-webkit-scrollbar { + display: none; +} + +.hiddenFileInput { + display: none; +} + +.subtitle { + margin: 0; + max-width: 760px; + color: var(--monitor-muted); + font-size: 13px; + line-height: 1.55; +} + +.mastheadActionButton { + flex: 0 0 auto; + align-self: center; + min-height: 36px; + padding: 0 14px; + border-radius: 10px; + border-color: transparent; + background: transparent; + color: var(--monitor-muted); + font-size: 13px; + font-weight: 700; +} + +.mastheadActionButton:hover { + border-color: color-mix(in srgb, var(--monitor-accent) 20%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-accent) 8%, var(--monitor-surface)); + color: var(--monitor-ink); + box-shadow: none; +} + +.segmentedControl { + display: inline-flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 4px; + padding: 3px; + border-radius: 999px; + border: 1px solid var(--monitor-line); + background: var(--monitor-surface-muted); +} + +.segmentButton, +.refreshButton, +.quickLinkButton, +.clearButton, +.inlineActionButton, +.quotaRefreshButton, +.accountButton, +.statusBadge { + transition: + transform 0.18s ease, + border-color 0.18s ease, + background-color 0.18s ease, + color 0.18s ease, + box-shadow 0.18s ease; +} + +.segmentButton { + min-height: 32px; + padding: 0 12px; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--monitor-muted); + font-size: 12px; + font-weight: 700; + cursor: pointer; +} + +.segmentButton:hover { + transform: translateY(-1px); + color: var(--monitor-ink); +} + +.segmentButtonActive { + background: var(--monitor-accent); + color: var(--primary-contrast, #fff); + box-shadow: 0 8px 18px -14px rgba(0, 0, 0, 0.45); +} + +.refreshCluster { + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-end; + gap: 6px; + padding: 4px; + border-radius: 10px; + border: 1px solid var(--monitor-line); + background: var(--monitor-surface-muted); +} + +.refreshControls { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: nowrap; +} + +.syncPill, +.quickLinkButton, +.clearButton, +.inlineActionButton, +.statusBadge { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 34px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid var(--monitor-line); + background: var(--monitor-surface-elevated); +} + +.syncPill, +.quickLinkButton { + color: var(--monitor-muted); + font-size: 12px; + white-space: nowrap; +} + +.syncPill { + min-height: 36px; + padding: 0 10px; +} + +.syncPill.tonegood { + border-color: color-mix(in srgb, var(--monitor-green) 30%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-green) 9%, var(--monitor-surface)); + color: var(--monitor-green); +} + +.syncPill.tonewarn { + border-color: color-mix(in srgb, var(--monitor-amber) 30%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-amber) 9%, var(--monitor-surface)); + color: var(--monitor-amber); +} + +.syncPill.tonebad { + border-color: color-mix(in srgb, var(--monitor-red) 30%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-red) 8%, var(--monitor-surface)); + color: var(--monitor-red); +} + +.quickLinkButton { + gap: 5px; + min-height: 32px; + padding: 0 10px; + font-size: 11px; +} + +.quickLinkButton { + text-decoration: none; + cursor: pointer; + font: inherit; + border-radius: 8px; +} + +.quickLinkButton:hover, +.clearButton:hover, +.inlineActionButton:hover, +.quotaRefreshButton:hover, +.refreshButton:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--monitor-accent) 35%, var(--monitor-line-strong)); + background: var(--monitor-surface-hover); + color: var(--monitor-ink); +} + +.clearButton, +.inlineActionButton { + gap: 8px; + color: var(--monitor-ink); + font-size: 12px; + font-weight: 700; + cursor: pointer; +} + +.autoRefreshField { + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 36px; + padding: 0 6px 0 10px; + border-radius: 12px; + border: 1px solid var(--monitor-line); + background: var(--monitor-surface); +} + +.autoRefreshLabel { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--monitor-muted); + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} + +.autoRefreshSelect { + width: 94px; + flex: 0 0 auto; +} + +.autoRefreshSelectTrigger { + min-width: 76px; + height: 30px !important; + padding: 0 2px 0 6px !important; + border: 0 !important; + border-radius: 10px !important; + background: transparent !important; + box-shadow: none !important; + color: var(--monitor-ink); + font-size: 12px; + font-weight: 600; + line-height: 1; +} + +.autoRefreshSelectTrigger:hover, +.autoRefreshSelectTrigger:focus, +.autoRefreshSelectTrigger[aria-expanded='true'] { + border: 0 !important; + background: transparent !important; + box-shadow: none !important; +} + +.refreshButton { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-width: 92px; + min-height: 36px; + padding: 0 12px; + border: 1px solid var(--monitor-line); + border-radius: 12px; + background: var(--monitor-surface); + color: var(--monitor-ink); + font: inherit; + font-size: 12px; + font-weight: 700; + cursor: pointer; + white-space: nowrap; +} + +.refreshButton:disabled { + cursor: not-allowed; + opacity: 0.72; + transform: none; +} + +.refreshIcon, +.refreshIconSpinning { + display: block; + flex-shrink: 0; + transform-origin: center; +} + +.refreshIconSpinning { + animation: monitor-spin 0.9s linear infinite; +} + +.refreshButtonLabel { + display: inline-flex; + align-items: center; + line-height: 1; +} + +.panel { + min-width: 0; + padding: 24px; +} + +.panelHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + margin-bottom: 16px; +} + +.panelTitle { + margin: 0; + font-size: 19px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.panelSubtitle { + margin: 6px 0 0; + color: var(--monitor-muted); + font-size: 13px; + line-height: 1.6; +} + +.panelExtra { + flex-shrink: 0; +} + +.toolbarHeaderActions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + width: min(420px, 44vw); + min-width: 0; +} + +.toolbarHeaderActions :global(.form-group) { + flex: 1 1 auto; + min-width: 0; + margin: 0; +} + +.toolbarHeaderActions :global(.form-group > div) { + display: flex; + align-items: center; +} + +.toolbarHeaderSearchInput { + width: 100%; + min-width: 0; + height: 40px; + min-height: 40px; + padding-block: 0; + padding-right: 36px; + border-radius: 8px; + font-size: 13px; + line-height: 1; +} + +.toolbarHeaderActions .clearButton { + flex: 0 0 auto; + min-height: 34px; + padding: 0 10px; + font-size: 11px; +} + +.pricingPanel, +.accountPanel { + background: var(--monitor-surface); +} + +.realtimePanel { + display: grid; + gap: 14px; + min-width: 0; + padding: 18px; + border: 1px solid var(--monitor-line); + border-radius: 12px; + background: var(--monitor-surface); + box-shadow: var(--shadow); +} + +.accountPanelActions { + min-width: 180px; +} + +.toolbarControlRow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; + padding: 12px 14px; + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 60%, transparent); + background: color-mix(in srgb, var(--monitor-surface-muted) 60%, transparent); +} + +.filterGrid { + display: grid; + grid-template-columns: minmax(260px, 1.4fr) repeat(4, minmax(128px, 1fr)) auto auto; + gap: 10px; + align-items: center; +} + +.filterGrid :global(.form-group) { + min-width: 0; + margin: 0; +} + +.filterGrid .clearButton { + min-height: 40px; + padding: 0 12px; + white-space: nowrap; +} + +.inlineMetrics { + display: flex; + flex: 1 1 auto; + flex-wrap: nowrap; + gap: 6px; + min-width: 0; + max-width: 100%; + overflow-x: auto; + scrollbar-width: none; +} + +.inlineMetrics::-webkit-scrollbar { + display: none; +} + +.inlineMetrics span { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid var(--monitor-line); + background: var(--monitor-surface-elevated); + color: var(--monitor-muted); + font-size: 12px; + white-space: nowrap; + flex: 0 0 auto; +} + +.realtimeLogStatusRow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-width: 0; +} + +.realtimeFollowToggle { + position: relative; + display: inline-flex; + align-items: center; + gap: 8px; + flex: 0 0 auto; + min-height: 30px; + color: var(--monitor-muted); + font-size: 12px; + cursor: pointer; +} + +.realtimeFollowToggle input { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.realtimeFollowTrack { + position: relative; + display: inline-flex; + width: 34px; + height: 18px; + flex: 0 0 auto; + border: 1px solid color-mix(in srgb, var(--monitor-muted) 44%, var(--monitor-line)); + border-radius: 999px; + background: var(--monitor-surface-muted); + transition: border-color 0.16s ease, background 0.16s ease; +} + +.realtimeFollowTrack > span { + position: absolute; + top: 2px; + left: 2px; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--monitor-muted); + transition: transform 0.16s ease, background 0.16s ease; +} + +.realtimeFollowToggle input:checked + .realtimeFollowTrack { + border-color: var(--monitor-accent-strong); + background: color-mix(in srgb, var(--monitor-accent) 28%, var(--monitor-surface)); +} + +.realtimeFollowToggle input:checked + .realtimeFollowTrack > span { + transform: translateX(16px); + background: var(--monitor-accent-strong); +} + +.realtimeFollowToggle input:focus-visible + .realtimeFollowTrack { + outline: 2px solid color-mix(in srgb, var(--monitor-accent-strong) 45%, transparent); + outline-offset: 2px; +} + +.realtimeFollowLabel { + color: var(--monitor-ink); + font-weight: 650; +} + +.realtimeUpdateBar { + position: absolute; + top: 50px; + right: 14px; + z-index: 6; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: min(460px, calc(100% - 28px)); + min-width: 0; + padding: 9px 11px; + border: 1px solid color-mix(in srgb, var(--monitor-accent) 30%, var(--monitor-line)); + border-radius: 8px; + background: color-mix(in srgb, var(--monitor-accent) 7%, var(--monitor-surface)); + box-shadow: var(--shadow); +} + +.realtimeUpdateCopy { + display: grid; + gap: 2px; + min-width: 0; +} + +.realtimeUpdateCopy strong { + color: var(--monitor-ink); + font-size: 12px; + line-height: 1.35; +} + +.realtimeUpdateCopy span { + color: var(--monitor-muted); + font-size: 11px; + line-height: 1.35; +} + +.realtimeUpdateBar .inlineActionButton { + flex: 0 0 auto; +} + +.realtimeTableShell { + position: relative; + min-width: 0; + height: min(620px, 68vh); +} + +.realtimeColumnsMenu { + position: relative; + min-width: 0; +} + +.realtimeColumnsDropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 10; + display: grid; + gap: 10px; + width: max-content; + min-width: 128px; + max-width: min(220px, calc(100vw - 32px)); + padding: 10px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); + border-radius: 8px; + background: var(--monitor-surface); + box-shadow: var(--shadow); +} + +.realtimeColumnsDropdownHeader { + display: grid; + gap: 8px; + color: var(--monitor-muted); + font-size: 12px; +} + +.realtimeColumnsDropdownHeader .inlineActionButton { + justify-self: start; +} + +.realtimeColumnsDropdownList { + display: grid; + gap: 4px; +} + +.realtimeColumnToggle { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; + min-height: 30px; + padding: 4px 6px; + border-radius: 6px; + color: var(--monitor-ink); + font-size: 12px; +} + +.realtimeColumnToggle:hover { + background: color-mix(in srgb, var(--monitor-accent) 8%, transparent); +} + +.realtimeColumnToggle input { + width: 14px; + height: 14px; + margin: 0; + accent-color: var(--monitor-accent-strong); +} + +.realtimeColumnToggle span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.errorBox, +.callout { + display: grid; + gap: 6px; + padding: 14px 16px; + border-radius: 18px; + border: 1px solid var(--monitor-line); +} + +.errorBox { + margin-top: 14px; + border-color: color-mix(in srgb, var(--monitor-red) 28%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-red) 10%, var(--monitor-surface)); + color: var(--monitor-red); +} + +.callout { + margin-top: 14px; + border-color: color-mix(in srgb, var(--monitor-accent) 24%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-accent) 8%, var(--monitor-surface)); +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_management.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_management.scss new file mode 100644 index 0000000..452424b --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_management.scss @@ -0,0 +1,1177 @@ +.monitoringSettingsEditor { + margin-top: 14px; + display: grid; + gap: 14px; +} + +.settingsSectionCard { + display: grid; + gap: 14px; + padding: 16px; + border: 1px solid color-mix(in srgb, var(--monitor-line-strong) 30%, var(--monitor-line)); + border-radius: 16px; + background: var(--monitor-surface-elevated); +} + +.settingsDangerSection { + border-color: color-mix(in srgb, var(--monitor-red) 32%, var(--monitor-line)); +} + +.settingsDangerAction { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding-top: 12px; + border-top: 1px solid color-mix(in srgb, var(--monitor-red) 18%, var(--monitor-line)); +} + +.settingsDangerAction > div { + display: grid; + gap: 3px; + min-width: 0; +} +.settingsDangerAction > div > span { + color: var(--monitor-muted); + font-size: 12px; +} + +.settingsDangerAction strong { + color: var(--monitor-ink); + font-size: 16px; +} + +.settingsDangerAction :global(button) { + flex: 0 0 auto; +} + +.resetStatisticsButton:global(.btn-danger) { + min-height: 40px; + padding-inline: 16px; + border-color: var(--monitor-red); + background: var(--monitor-red); + color: #fff; + box-shadow: 0 8px 18px -12px color-mix(in srgb, var(--monitor-red) 78%, transparent); +} + +.resetStatisticsButton:global(.btn-danger) > span { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + color: inherit; + line-height: 1.2; +} + +.resetStatisticsButton:global(.btn-danger) svg { + flex: 0 0 auto; + color: inherit; + stroke: currentColor; +} + +.resetStatisticsButton:global(.btn-danger):hover:not(:disabled) { + border-color: color-mix(in srgb, var(--monitor-red) 86%, #000); + background: color-mix(in srgb, var(--monitor-red) 86%, #000); + color: #fff; + box-shadow: 0 10px 22px -12px color-mix(in srgb, var(--monitor-red) 88%, transparent); + transform: translateY(-1px); +} + +.resetStatisticsButton:global(.btn-danger):focus-visible { + outline: 3px solid color-mix(in srgb, var(--monitor-red) 28%, transparent); + outline-offset: 2px; +} + +.resetStatisticsButton:global(.btn-danger):disabled { + color: #fff; + opacity: 0.68; + transform: none; +} + +.settingsSectionHeader { + display: grid; + gap: 4px; +} + +.settingsSectionHeader strong { + color: var(--monitor-ink); + font-size: 14px; + font-weight: 800; +} + +.settingsSectionHeader span, +.settingsField small, +.settingsHint, +.settingsScheduleNote { + color: var(--monitor-muted); + font-size: 12px; + line-height: 1.5; +} + +.settingsField { + display: grid; + align-content: start; + gap: 8px; + min-width: 0; + color: var(--monitor-ink); + font-size: 13px; + font-weight: 700; +} + +.settingsField :global(.form-group) { + margin-bottom: 0; +} + +.settingsField input { + min-height: 42px; +} + +.settingsScheduleNote { + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--monitor-accent) 18%, var(--monitor-line)); + border-radius: 12px; + background: color-mix(in srgb, var(--monitor-accent) 7%, var(--monitor-surface)); +} + +.settingsGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: start; + gap: 12px; +} + +.settingsCheckboxField { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--monitor-ink); + font-size: 13px; + font-weight: 700; +} + +.settingsCheckboxField input { + width: 16px; + height: 16px; + accent-color: var(--monitor-accent); +} + +.priceField { + display: grid; + gap: 7px; + min-width: 0; + color: var(--monitor-muted); + font-size: 12px; + font-weight: 700; +} + +.priceField :global(.form-group) { + margin-bottom: 0; +} + +.priceField input { + min-height: 40px; +} + +.priceActionsBar { + margin-top: 2px; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.priceManagerModal :global(.modal-body) { + max-height: min(72vh, 640px); + overflow: hidden; + padding: 0; +} + +.priceManager { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + background: var(--monitor-surface); +} + +.priceManagerTabs { + display: flex; + align-items: center; + gap: 6px; + min-height: 52px; + padding: 8px 20px 0; + border-bottom: 1px solid var(--monitor-line); +} + +.priceManagerTab { + position: relative; + display: inline-flex; + align-items: center; + gap: 8px; + align-self: stretch; + min-width: 112px; + padding: 0 14px 7px; + border: 0; + background: transparent; + color: var(--monitor-muted); + font-size: 13px; + font-weight: 700; + cursor: pointer; +} + +.priceManagerTab::after { + content: ''; + position: absolute; + right: 10px; + bottom: -1px; + left: 10px; + height: 2px; + border-radius: 2px 2px 0 0; + background: transparent; +} + +.priceManagerTab > span { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 20px; + padding: 0 6px; + border-radius: 999px; + background: var(--monitor-surface-muted); + color: var(--monitor-muted); + font-size: 11px; +} + +.priceManagerTab:hover { + color: var(--monitor-ink); +} + +.priceManagerTabActive { + color: var(--monitor-accent); +} + +.priceManagerTabActive::after { + background: var(--monitor-accent); +} + +.priceManagerTabActive > span { + background: color-mix(in srgb, var(--monitor-accent) 13%, var(--monitor-surface)); + color: var(--monitor-accent); +} + +.priceRuleWorkspace { + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + height: min(64vh, 560px); + min-height: 0; + overflow: hidden; +} + +.priceRuleSidebar { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + min-width: 0; + overflow: hidden; + border-right: 1px solid var(--monitor-line); + background: color-mix(in srgb, var(--monitor-surface-muted) 68%, var(--monitor-surface)); +} + +.priceRuleSearch { + position: relative; + padding: 14px; + border-bottom: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); +} + +.priceRuleSearch > svg { + position: absolute; + z-index: 1; + top: 50%; + left: 26px; + color: var(--monitor-muted); + pointer-events: none; + transform: translateY(-50%); +} + +.priceRuleSearch :global(.form-group) { + margin: 0; +} + +.priceRuleSearch input { + min-height: 38px; + padding-left: 34px; + background: var(--monitor-surface); +} + +.priceRuleList { + min-height: 0; + overflow: auto; + overscroll-behavior: contain; + padding: 8px; +} + +.priceRuleListItem { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + width: 100%; + min-height: 58px; + padding: 10px 11px; + border: 1px solid transparent; + border-radius: 8px; + background: transparent; + color: var(--monitor-ink); + text-align: left; + cursor: pointer; +} + +.priceRuleListItem:hover { + background: var(--monitor-surface-hover); +} + +.priceRuleListItemActive { + border-color: color-mix(in srgb, var(--monitor-accent) 32%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-accent) 9%, var(--monitor-surface)); +} + +.priceRuleListIdentity, +.priceRuleListMeta { + display: grid; + gap: 4px; + min-width: 0; +} + +.priceRuleListIdentity strong, +.priceUnmatchedList strong { + overflow: hidden; + font-size: 13px; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.priceRuleListIdentity small, +.priceRuleListMeta small, +.priceUnmatchedList small { + overflow: hidden; + color: var(--monitor-muted); + font-size: 11px; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.priceRuleListMeta { + justify-items: end; + text-align: right; +} + +.priceRuleConfigured, +.priceRuleUnconfigured { + display: inline-flex; + align-items: center; + width: max-content; + min-height: 20px; + padding: 0 7px; + border-radius: 999px; + font-size: 10px; + font-weight: 800; + white-space: nowrap; +} + +.priceRuleConfigured { + background: color-mix(in srgb, var(--monitor-green) 13%, var(--monitor-surface)); + color: color-mix(in srgb, var(--monitor-green) 82%, var(--monitor-ink)); +} + +.priceRuleUnconfigured { + background: color-mix(in srgb, var(--monitor-amber) 14%, var(--monitor-surface)); + color: color-mix(in srgb, var(--monitor-amber) 76%, var(--monitor-ink)); +} + +.priceRuleListEmpty, +.priceRuleEditorEmpty { + display: grid; + place-items: center; + min-height: 140px; + padding: 24px; + color: var(--monitor-muted); + font-size: 13px; + text-align: center; +} + +.priceRuleEditorEmpty { + grid-row: 1 / -1; +} + +.priceRuleEditorPane { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +.priceRuleEditorHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-height: 72px; + padding: 14px 20px; + border-bottom: 1px solid var(--monitor-line); +} + +.priceRuleEditorHeader > div:first-child { + display: grid; + gap: 3px; + min-width: 0; +} + +.priceRuleEditorHeader h3, +.priceSyncHeader h3 { + overflow: hidden; + margin: 0; + color: var(--monitor-ink); + font-size: 16px; + font-weight: 750; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.priceRuleEditorHeader > div:first-child > span, +.priceSyncHeader > div:first-child > div > span { + color: var(--monitor-muted); + font-size: 12px; +} + +.priceRuleEditorBadges { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 6px; +} + +.priceRuleEditorBadges > span:not(.priceRuleConfigured):not(.priceRuleUnconfigured) { + display: inline-flex; + align-items: center; + min-height: 20px; + padding: 0 7px; + border: 1px solid var(--monitor-line); + border-radius: 999px; + color: var(--monitor-muted); + font-size: 10px; + font-weight: 700; +} + +.priceRuleEditorScroll { + min-height: 0; + overflow: auto; + overscroll-behavior: contain; +} + +.priceRuleSection { + display: grid; + gap: 16px; + padding: 20px; + border-bottom: 1px solid var(--monitor-line); +} + +.priceRuleSection:last-child { + border-bottom: 0; +} + +.priceRuleSectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.priceRuleSectionHeader > div { + display: flex; + align-items: baseline; + gap: 8px; + min-width: 0; +} + +.priceRuleSectionHeader h4 { + margin: 0; + color: var(--monitor-ink); + font-size: 13px; + font-weight: 800; +} + +.priceRuleSectionHeader span { + color: var(--monitor-muted); + font-size: 11px; + font-weight: 650; +} + +.priceBaseGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.priceTierList { + display: grid; + gap: 8px; +} + +.priceTierCompactRow { + display: grid; + grid-template-columns: 26px minmax(130px, 1.3fr) repeat(4, minmax(90px, 1fr)) 32px; + align-items: end; + gap: 8px; + padding: 10px; + border: 1px solid var(--monitor-line); + border-radius: 8px; + background: color-mix(in srgb, var(--monitor-surface-muted) 64%, var(--monitor-surface)); +} + +.priceTierCompactRow label { + display: grid; + gap: 6px; + min-width: 0; +} + +.priceTierCompactRow label > span { + display: flex; + align-items: flex-end; + min-height: 24px; + color: var(--monitor-muted); + font-size: 10px; + font-weight: 700; + line-height: 1.2; +} + +.priceTierCompactRow :global(.form-group) { + margin: 0; +} + +.priceTierCompactRow input { + min-height: 36px; + padding-inline: 9px; + font-size: 12px; +} + +.priceTierIndex { + display: inline-flex; + align-items: center; + justify-content: center; + align-self: end; + width: 24px; + height: 36px; + color: var(--monitor-muted); + font-size: 11px; + font-weight: 800; +} + +.priceTierRemoveButton { + display: inline-flex; + align-items: center; + justify-content: center; + align-self: end; + width: 32px; + height: 36px; + padding: 0; + border: 1px solid transparent; + border-radius: 7px; + background: transparent; + color: var(--monitor-muted); + cursor: pointer; +} + +.priceTierRemoveButton:hover { + border-color: color-mix(in srgb, var(--monitor-red) 20%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-red) 8%, var(--monitor-surface)); + color: var(--monitor-red); +} + +.priceTierEmpty { + display: grid; + place-items: center; + min-height: 82px; + padding: 16px; + border: 1px dashed var(--monitor-line); + border-radius: 8px; + color: var(--monitor-muted); + font-size: 12px; + text-align: center; +} + +.priceRuleEditorFooter { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 64px; + padding: 12px 20px; + border-top: 1px solid var(--monitor-line); + background: var(--monitor-surface); +} + +.priceRuleEditorFooter > div { + display: flex; + align-items: center; + gap: 8px; +} + +.priceSyncView { + display: grid; + grid-auto-rows: max-content; + align-content: start; + gap: 16px; + min-height: 0; + max-height: min(64vh, 560px); + overflow: auto; + padding: 20px; +} + +.priceSyncHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + min-height: 52px; +} + +.priceSyncHeader > div:first-child { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.priceSyncHeader > div:first-child > div { + display: grid; + gap: 4px; + min-width: 0; +} + +.priceSyncStatusDot { + flex: 0 0 auto; + width: 10px; + height: 10px; + border-radius: 999px; + background: var(--monitor-muted); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--monitor-muted) 12%, transparent); +} + +.priceSyncStatussyncing { + background: var(--monitor-accent); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--monitor-accent) 14%, transparent); + animation: monitor-pulse 1.4s ease-in-out infinite; +} + +.priceSyncStatussuccess { + background: var(--monitor-green); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--monitor-green) 14%, transparent); +} + +.priceSyncStatuserror { + background: var(--monitor-red); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--monitor-red) 14%, transparent); +} + +.priceSyncActions { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 8px; +} + +.priceSyncActions :global(.btn) { + min-height: 38px; +} + +.priceSyncApplyButton:global(.btn-primary) { + border-color: #2563eb; + background: #2563eb; + color: #fff; +} + +.priceSyncApplyButton:global(.btn-primary):hover:not(:disabled) { + border-color: #1d4ed8; + background: #1d4ed8; + color: #fff; +} + +.priceSyncApplyButton:global(.btn-primary):disabled { + border-color: var(--monitor-line-strong); + background: var(--monitor-surface-muted); + color: var(--monitor-muted); + opacity: 1; +} + +.priceSyncApplyButton:global(.btn) > span { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + color: inherit; + line-height: 1; +} + +.priceSyncApplyButton:global(.btn) > span svg, +.priceSyncApplyIcon { + display: block; + flex: 0 0 auto; + color: inherit; + fill: none; + stroke: currentColor; +} + +.priceSyncMetrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-auto-rows: minmax(78px, max-content); + min-height: 80px; + overflow: hidden; + border: 1px solid var(--monitor-line); + border-radius: 8px; + background: var(--monitor-surface); +} + +.priceSyncMetrics > div { + display: flex; + flex-direction: column; + justify-content: center; + gap: 6px; + min-height: 78px; + padding: 11px 14px; + border-right: 1px solid var(--monitor-line); + background: color-mix(in srgb, var(--monitor-surface-elevated) 46%, var(--monitor-surface)); +} + +.priceSyncMetrics > div:last-child { + border-right: 0; +} + +.priceSyncMetrics span { + color: var(--monitor-muted); + font-size: 11px; + font-weight: 700; +} + +.priceSyncMetrics strong { + color: var(--monitor-ink); + font-size: 22px; + font-weight: 750; + font-variant-numeric: tabular-nums; + line-height: 1.2; +} + +.priceSyncMetricadded strong { + color: var(--monitor-green); +} + +.priceSyncMetricupdated strong { + color: var(--monitor-accent); +} + +.priceSyncMetricunmatched strong { + color: var(--monitor-amber); +} + +.priceSyncError { + padding: 11px 13px; + border: 1px solid color-mix(in srgb, var(--monitor-red) 24%, var(--monitor-line)); + border-radius: 8px; + background: color-mix(in srgb, var(--monitor-red) 7%, var(--monitor-surface)); + color: var(--monitor-red); + font-size: 12px; + line-height: 1.5; +} + +.priceSyncSchedule { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-top: 14px; + border-top: 1px solid var(--monitor-line); +} + +.priceSyncSchedule > div:first-child { + display: grid; + gap: 3px; + min-width: 0; +} + +.priceSyncSchedule > div:first-child strong { + color: var(--monitor-ink); + font-size: 12px; + font-weight: 800; +} + +.priceSyncSchedule > div:first-child span, +.priceSyncScheduleInterval > span { + color: var(--monitor-muted); + font-size: 10px; + line-height: 1.35; +} + +.priceSyncScheduleControls { + display: flex; + flex: 0 0 auto; + align-items: flex-end; + gap: 10px; +} + +.priceSyncScheduleToggle { + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--monitor-ink); + font-size: 11px; + font-weight: 700; + min-height: 36px; + white-space: nowrap; +} + +.priceSyncScheduleToggle input { + width: 16px; + height: 16px; + accent-color: var(--monitor-accent); +} + +.priceSyncScheduleInterval { + display: grid; + gap: 4px; + width: 138px; +} + +.priceSyncScheduleInterval :global(.form-group) { + margin: 0; +} + +.priceSyncScheduleInterval input { + min-height: 34px; +} + +.priceSyncChangesSection { + display: grid; + gap: 10px; + min-width: 0; +} + +.priceSyncChangesHeader { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; +} + +.priceSyncChangesHeader > div:first-child { + display: grid; + gap: 3px; + min-width: 0; +} + +.priceSyncChangesHeader h4 { + margin: 0; + color: var(--monitor-ink); + font-size: 13px; + font-weight: 800; +} + +.priceSyncChangesHeader > div:first-child > span { + color: var(--monitor-muted); + font-size: 11px; +} + +.priceSyncChangesToolbar { + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 8px; + min-width: 0; +} + +.priceSyncOverrideAll, +.priceSyncOverrideOption { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--monitor-muted); + font-size: 10px; + font-weight: 700; + white-space: nowrap; + cursor: pointer; +} + +.priceSyncOverrideAll { + min-height: 34px; + padding: 0 9px; + border: 1px solid var(--monitor-line); + border-radius: 8px; + background: var(--monitor-surface-muted); +} + +.priceSyncOverrideAll:has(input:checked) { + border-color: color-mix(in srgb, var(--monitor-accent) 34%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-accent) 8%, var(--monitor-surface)); + color: var(--monitor-accent); +} + +.priceSyncOverrideAll input, +.priceSyncOverrideOption input { + width: 15px; + height: 15px; + margin: 0; + accent-color: var(--monitor-accent); +} + +.priceSyncChangeFilters { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + gap: 3px; + padding: 3px; + border: 1px solid var(--monitor-line); + border-radius: 8px; + background: var(--monitor-surface-muted); +} + +.priceSyncChangeFilters button { + display: inline-flex; + align-items: center; + gap: 5px; + min-height: 30px; + padding: 0 8px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--monitor-muted); + font-size: 10px; + font-weight: 750; + line-height: 1; + white-space: nowrap; + cursor: pointer; +} + +.priceSyncChangeFilterLabel { + display: inline-flex; + align-items: center; + height: 18px; + line-height: 18px; +} + +.priceSyncChangeFilterCount { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 999px; + background: color-mix(in srgb, var(--monitor-muted) 10%, transparent); + color: inherit; + font-size: 9px; + font-variant-numeric: tabular-nums; + line-height: 18px; +} + +.priceSyncChangeFilters .priceSyncChangeFilterActive { + background: var(--monitor-surface); + color: var(--monitor-ink); + box-shadow: 0 1px 3px color-mix(in srgb, var(--monitor-ink) 12%, transparent); +} + +.priceSyncChangeFilters .priceSyncChangeFilterActive .priceSyncChangeFilterCount { + background: color-mix(in srgb, var(--monitor-accent) 12%, var(--monitor-surface)); + color: var(--monitor-accent); +} + +.priceSyncChangeList { + display: grid; + overflow: hidden; + border: 1px solid var(--monitor-line); + border-radius: 8px; + background: var(--monitor-surface); +} + +.priceSyncChangeRow { + display: grid; + grid-template-columns: minmax(220px, 0.8fr) minmax(0, 1.2fr); + align-items: center; + gap: 16px; + min-height: 62px; + padding: 10px 12px; + border-bottom: 1px solid var(--monitor-line); +} + +.priceSyncChangeRow:last-child { + border-bottom: 0; +} + +.priceSyncChangeIdentity { + display: flex; + align-items: center; + gap: 9px; + min-width: 0; +} + +.priceSyncChangeIdentity > div { + display: grid; + gap: 3px; + min-width: 0; +} + +.priceSyncChangeIdentity strong { + overflow: hidden; + color: var(--monitor-ink); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.priceSyncChangeIdentity small, +.priceSyncChangeHint, +.priceSyncRateChanges > small { + overflow: hidden; + color: var(--monitor-muted); + font-size: 10px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.priceSyncChangeBadge { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + justify-content: center; + min-width: 46px; + min-height: 22px; + padding: 0 7px; + border-radius: 999px; + font-size: 9px; + font-weight: 800; +} + +.priceSyncChangeadded { + background: color-mix(in srgb, var(--monitor-green) 13%, var(--monitor-surface)); + color: var(--monitor-green); +} + +.priceSyncChangeupdated { + background: color-mix(in srgb, var(--monitor-accent) 12%, var(--monitor-surface)); + color: var(--monitor-accent); +} + +.priceSyncChangeoverridden { + background: color-mix(in srgb, var(--monitor-accent) 16%, var(--monitor-surface)); + color: color-mix(in srgb, var(--monitor-accent) 82%, var(--monitor-ink)); +} + +.priceSyncChangelocked { + background: color-mix(in srgb, var(--monitor-muted) 12%, var(--monitor-surface)); + color: var(--monitor-muted); +} + +.priceSyncChangeunmatched { + background: color-mix(in srgb, var(--monitor-amber) 14%, var(--monitor-surface)); + color: color-mix(in srgb, var(--monitor-amber) 78%, var(--monitor-ink)); +} + +.priceSyncRateChanges { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 6px 14px; + min-width: 0; +} + +.priceSyncRateChanges > div { + display: grid; + gap: 2px; + min-width: 84px; +} + +.priceSyncRateChanges > div > span { + color: var(--monitor-muted); + font-size: 9px; +} + +.priceSyncRateChanges > div > div { + display: flex; + align-items: baseline; + gap: 5px; + font-size: 10px; +} + +.priceSyncRateChanges del { + color: var(--monitor-muted); +} + +.priceSyncRateChanges strong { + color: var(--monitor-ink); + font-size: 11px; +} + +.priceSyncOverrideOption { + min-height: 30px; + padding: 0 8px; + border: 1px solid var(--monitor-line); + border-radius: 7px; + background: var(--monitor-surface-muted); + color: var(--monitor-ink); +} + +.priceSyncOverrideOption:has(input:checked) { + border-color: color-mix(in srgb, var(--monitor-accent) 34%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-accent) 8%, var(--monitor-surface)); + color: var(--monitor-accent); +} + +.priceSyncChangesEmpty { + display: grid; + place-items: center; + min-height: 72px; + padding: 14px; + color: var(--monitor-muted); + font-size: 11px; + text-align: center; +} + +.priceSyncResultSection { + display: grid; + gap: 12px; +} + +.priceUnmatchedList { + display: grid; + overflow: hidden; + border: 1px solid var(--monitor-line); + border-radius: 8px; +} + +.priceUnmatchedList > div:not(.priceTierEmpty) { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-height: 54px; + padding: 9px 13px; + border-bottom: 1px solid var(--monitor-line); +} + +.priceUnmatchedList > div:last-child { + border-bottom: 0; +} + +.priceUnmatchedList > .priceTierEmpty { + border: 0; + border-radius: 0; +} + +.priceUnmatchedList > div > span { + display: grid; + gap: 3px; + min-width: 0; +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_realtime.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_realtime.scss new file mode 100644 index 0000000..2c07708 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_realtime.scss @@ -0,0 +1,1291 @@ +.tableWrapper { + position: relative; + min-width: 0; + max-width: 100%; + overflow-x: auto; + scrollbar-gutter: stable; +} + +.tableScrollWrapper { + max-height: min(560px, 64vh); + overflow: auto; + overscroll-behavior: contain; + padding-right: 0; + border: 1px solid var(--monitor-line); + border-radius: 8px; +} + +.accountTableWrapper { + max-height: min(520px, 58vh); +} + +.realtimeTableWrapper { + height: 100%; + max-height: none; + overflow-x: auto; + overflow-y: auto; + overflow-anchor: none; + touch-action: pan-x pan-y; + -webkit-overflow-scrolling: touch; +} + +.tableScrollWrapper::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.tableScrollWrapper::-webkit-scrollbar-thumb { + border: 3px solid transparent; + border-radius: 999px; + background: color-mix(in srgb, var(--monitor-muted) 34%, transparent); + background-clip: padding-box; +} + +.tableScrollWrapper::-webkit-scrollbar-track { + background: transparent; +} + +.tableWrapper::after { + content: ''; + position: sticky; + right: 0; + display: block; + width: 36px; + height: 1px; + margin-top: -1px; + margin-left: auto; + pointer-events: none; + background: linear-gradient(90deg, transparent, var(--monitor-surface)); +} + +.table { + width: 100%; + min-width: 0; + border-collapse: collapse; + border-spacing: 0; +} + +.accountOverviewTable { + --account-col-account: 24%; + --account-col-total-calls: 9%; + --account-col-success-calls: 10%; + --account-col-success-rate: 9%; + --account-col-total-tokens: 12%; + --account-col-estimated-cost: 10%; + --account-col-latest-request: 17%; + --account-col-action: 9%; + --account-overview-columns: + var(--account-col-account) + var(--account-col-total-calls) + var(--account-col-success-calls) + var(--account-col-success-rate) + var(--account-col-total-tokens) + var(--account-col-estimated-cost) + var(--account-col-latest-request) + var(--account-col-action); + + min-width: 0; + table-layout: fixed; +} + +.accountOverviewTable col:nth-child(1) { + width: var(--account-col-account); +} + +.accountOverviewTable col:nth-child(2) { + width: var(--account-col-total-calls); +} + +.accountOverviewTable col:nth-child(3) { + width: var(--account-col-success-calls); +} + +.accountOverviewTable col:nth-child(4) { + width: var(--account-col-success-rate); +} + +.accountOverviewTable col:nth-child(5) { + width: var(--account-col-total-tokens); +} + +.accountOverviewTable col:nth-child(6) { + width: var(--account-col-estimated-cost); +} + +.accountOverviewTable col:nth-child(7) { + width: var(--account-col-latest-request); +} + +.accountOverviewTable col:nth-child(8) { + width: var(--account-col-action); +} + +.accountOverviewTable .accountButtonLabel { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} + +.accountOverviewTable .accountButton small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accountOverviewTable td:nth-child(7), +.accountOverviewTable th:nth-child(7) { + white-space: normal; +} + +.accountOverviewTable td:nth-child(7) { + font-size: 12px; + line-height: 1.35; +} + +.accountOverviewTable td:last-child { + min-width: 0; +} + +.accountOverviewTable td:last-child .inlineActionButton { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.realtimeTable { + width: var(--realtime-table-min-width, max-content); + min-width: var(--realtime-table-min-width, max-content); + table-layout: fixed; +} + +.table thead th { + position: sticky; + top: 0; + z-index: 3; + padding: 10px 12px; + background: var(--monitor-surface-muted); + text-align: left; + color: var(--monitor-ink); + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + text-transform: none; + white-space: normal; + line-height: 1.35; +} + +.table thead th::after { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 1px; + background: var(--monitor-line); +} + +.sortableHeaderButton { + width: 100%; + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 3px; + min-width: 0; + padding: 0; + border: 0; + background: transparent; + color: inherit; + font: inherit; + letter-spacing: inherit; + text-align: left; + text-transform: inherit; + cursor: pointer; +} + +.sortableHeaderButton:hover { + transform: none; + color: var(--monitor-ink); +} + +.sortableHeaderButton:focus, +.sortableHeaderButton:focus-visible { + outline: none; + box-shadow: none; +} + +.sortableHeaderButtonActive { + color: var(--monitor-ink); +} + +.sortIndicator { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + min-width: 14px; + height: 14px; + color: var(--monitor-accent-strong); +} + +.sortIndicator svg { + display: block; +} + +.table tbody td { + min-width: 0; + padding: 10px 12px; + vertical-align: top; + white-space: nowrap; + border-top: 1px solid var(--monitor-line); + background: var(--monitor-surface); + font-size: 13px; + line-height: 1.45; +} + +.realtimeTable tbody td { + vertical-align: middle; +} + +.table tbody td:first-child { + border-left: 0; + border-radius: 0; +} + +.table tbody td:last-child { + border-right: 0; +} + +//.table tbody tr:hover td { +// background: var(--monitor-surface-hover); +//} + +.table tbody tr:nth-child(2n) td { + background: color-mix(in srgb, var(--monitor-surface-muted) 72%, var(--monitor-surface)); +} + +.innerTable { + min-width: 940px; +} + +.primaryCell { + display: grid; + gap: 4px; + min-width: 0; +} + +.primaryCell span, +.primaryCell small { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.35; +} + +.primaryCell span { + font-size: 13px; + font-weight: 600; +} + +.realtimeTimeCell { + white-space: normal; + line-height: 1.35; + font-size: 12px; + color: var(--monitor-ink); +} + +.realtimeNowrapCell { + white-space: nowrap; +} + +.realtimeMetricCell { + text-align: left; +} + +.table thead th.realtimeMetricHeader { + text-align: left; +} + +.table thead th.realtimeCenterHeader, +.realtimeCenterCell { + text-align: left; +} + +.realtimeCenterCell > * { + margin-inline: 0; +} + +.realtimeCostCell { + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 6px; + width: 100%; + min-height: 54px; + font-variant-numeric: tabular-nums; +} + +.realtimeCostValue { + overflow: visible; + color: var(--monitor-green); + font-weight: 750; + line-height: 1.2; + text-overflow: clip; +} + +.realtimeCostInfoButton { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: 0; + border-radius: 50%; + background: transparent; + color: var(--monitor-muted); + cursor: help; +} + +.realtimeCostInfoButton:hover, +.realtimeCostInfoButton:focus-visible { + background: color-mix(in srgb, var(--monitor-accent) 10%, transparent); + color: var(--monitor-accent-strong); + outline: none; +} + +.realtimeCostInfoButton svg { + display: block; +} + +.realtimeCostTooltip { + position: fixed; + z-index: 12000; + width: min(336px, calc(100vw - 24px)); + max-height: min(420px, calc(100vh - 24px)); + overflow: visible; + padding: 18px 20px; + border: 1px solid #334155; + border-radius: 12px; + background: #060b1a; + color: #f8fafc; + box-shadow: 0 20px 48px -20px rgba(2, 6, 23, 0.72); + pointer-events: none; +} + +.realtimeCostTooltip::before { + content: ''; + position: absolute; + top: var(--realtime-cost-arrow-top, 50%); + width: 14px; + height: 14px; + border: solid #334155; + background: #060b1a; + transform: translateY(-50%) rotate(45deg); +} + +.realtimeCostTooltip[data-placement='left']::before { + right: -8px; + border-width: 1px 1px 0 0; +} + +.realtimeCostTooltip[data-placement='right']::before { + left: -8px; + border-width: 0 0 1px 1px; +} + +.realtimeCostTooltipTitle { + display: block; + margin-bottom: 12px; + color: #cbd5e1; + font-size: 15px; + font-weight: 800; + line-height: 1.2; +} + +.realtimeCostTooltipRows { + display: grid; + gap: 9px; + max-height: min(340px, calc(100vh - 90px)); + overflow: auto; + padding-right: 2px; +} + +.realtimeCostTooltipRows > div:not(.realtimeCostTooltipDivider) { + display: grid; + grid-template-columns: minmax(0, 1fr) max-content; + align-items: baseline; + gap: 14px; +} + +.realtimeCostTooltipRows span { + color: #94a3b8; + font-size: 12px; + line-height: 1.35; +} + +.realtimeCostTooltipRows strong { + color: #f8fafc; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; + font-weight: 600; + letter-spacing: 0; + line-height: 1.35; + text-align: right; + white-space: nowrap; +} + +.realtimeCostTooltipRows .realtimeCostRateInput { + color: #38bdf8; +} + +.realtimeCostTooltipRows .realtimeCostRateOutput { + color: #c4b5fd; +} + +.realtimeCostTooltipDivider { + height: 1px; + margin: 2px 0; + background: #1e293b; +} + +.realtimeCostTooltipEmpty { + margin: 0; + color: #94a3b8; + font-size: 12px; + line-height: 1.55; +} + +.realtimeDraggableHeader { + position: relative; + white-space: nowrap; + cursor: grab; +} + +.realtimeDraggableHeader:active { + cursor: grabbing; +} + +.realtimeFixedHeader, +.realtimeFixedHeader:active { + cursor: default; +} + +.realtimeDraggableHeaderActive { + color: var(--monitor-accent-strong); + background: color-mix(in srgb, var(--monitor-accent) 14%, var(--monitor-surface-muted)); +} + +.realtimeHeaderContent { + display: inline-block; + max-width: calc(100% - 12px); + overflow: visible; + white-space: nowrap; + vertical-align: middle; +} + +.realtimeColumnResizeHandle { + position: absolute; + top: 0; + right: -4px; + bottom: 0; + z-index: 4; + width: 8px; + cursor: col-resize; + touch-action: none; +} + +.realtimeColumnResizeHandle::after { + content: ''; + position: absolute; + top: 9px; + right: 3px; + bottom: 9px; + width: 1px; + background: transparent; +} + +.realtimeColumnResizeHandle:hover::after { + background: var(--monitor-accent-strong); +} + +.realtimeMetricCell { + vertical-align: middle; + white-space: nowrap; +} + +.realtimeReasoningCol { + min-width: 96px; +} + +.realtimeStreamCol { + min-width: 92px; +} + +.realtimeReasoningBadge { + display: inline-flex; + max-width: 100%; +} + +.realtimeTokensTableCell { + white-space: normal; +} + +.realtimeCacheReadTableCell { + text-align: left; + white-space: normal; +} + +.realtimeStatusErrorButton { + display: inline-flex; + align-items: center; + justify-content: flex-start; + width: 100%; + min-width: 0; + padding: 0; + border: 0; + border-radius: 999px; + background: transparent; + color: inherit; + cursor: pointer; +} + +.realtimeStatusErrorButton:hover { + transform: none; + filter: brightness(0.97); +} + +.realtimeStatusErrorButton:focus-visible { + outline: 2px solid color-mix(in srgb, var(--monitor-red) 42%, var(--monitor-line)); + outline-offset: 2px; + border-radius: 999px; +} + +.realtimeStatusErrorButton .statusBadge { + width: 100%; +} + +.realtimeErrorDetailsPanel { + display: grid; + gap: 14px; + padding: 2px 0 4px; +} + +.monitorModalActions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; +} + +.realtimeErrorOverview { + display: grid; + gap: 10px; + min-width: 0; + padding: 14px 16px; + border: 1px solid color-mix(in srgb, var(--monitor-red) 24%, var(--monitor-line)); + border-radius: 10px; + background: color-mix(in srgb, var(--monitor-red) 7%, var(--monitor-surface)); +} + +.realtimeErrorOverviewTop { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + min-width: 0; +} + +.realtimeErrorOverviewTop .statusBadge { + width: auto; +} + +.realtimeErrorOverviewTop span { + color: var(--monitor-red); + font-size: 12px; + font-weight: 650; + line-height: 1.35; +} + +.realtimeErrorOverview strong { + color: var(--monitor-ink); + font-size: 15px; + font-weight: 650; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.realtimeErrorMessageBlock { + display: grid; + gap: 6px; +} + +.realtimeErrorMessageBlock > span { + color: var(--monitor-muted); + font-size: 12px; + font-weight: 600; + line-height: 1.35; +} + +.realtimeErrorMessage { + max-height: 180px; + overflow: auto; + margin: 0; + padding: 12px 14px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 86%, transparent); + border-radius: 8px; + background: color-mix(in srgb, var(--monitor-surface-muted) 58%, var(--monitor-surface)); + color: var(--monitor-ink); + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + font-size: 12px; + line-height: 1.5; + white-space: pre-wrap; +} + +.realtimeErrorDetailsGrid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 0; + border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); + border-radius: 8px; + overflow: hidden; +} + +.realtimeErrorDetailItem { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + min-width: 0; + padding: 9px 12px; + background: var(--monitor-surface); +} + +.realtimeErrorDetailItem + .realtimeErrorDetailItem { + border-top: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); +} + +.realtimeErrorDetailItem span { + flex: 0 0 auto; + color: var(--monitor-muted); + font-size: 12px; + line-height: 1.35; +} + +.realtimeErrorDetailItem strong { + min-width: 0; + max-width: 72%; + text-align: right; + overflow-wrap: anywhere; + color: var(--monitor-ink); + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + font-size: 12px; + font-weight: 600; + line-height: 1.35; +} + +.realtimeTokenCell { + width: 100%; + min-height: 54px; + align-content: center; + gap: 3px; +} + +.realtimeTokenCell > span, +.realtimeTokenCell > small { + display: block; + overflow: visible; + text-overflow: clip; + white-space: nowrap; +} + +.realtimeTokenCell > span { + color: var(--monitor-muted); + font-size: 12px; + font-weight: 700; +} + +.realtimeTokenCell > span strong { + color: var(--monitor-ink); + font-size: 15px; + font-weight: 800; + font-variant-numeric: tabular-nums; +} + +.realtimeTokenCell > small { + color: var(--monitor-muted); + font-size: 11px; + font-weight: 600; +} + +.realtimeCacheReadCell { + display: grid; + align-content: center; + justify-items: start; + width: 100%; + min-height: 54px; + gap: 2px; + font-variant-numeric: tabular-nums; +} + +.realtimeCacheReadCell strong { + color: var(--monitor-ink); + font-size: 15px; + font-weight: 800; + line-height: 1.3; +} + +.realtimeCacheReadCell small { + color: var(--monitor-muted); + font-size: 11px; + font-weight: 650; + line-height: 1.35; + white-space: nowrap; +} + +.realtimeCacheReadCell .realtimeCacheHitLow { + color: var(--monitor-red); +} + +.primaryCell small, +.statusCell small { + color: var(--monitor-muted); + font-size: 11px; +} + +.monoCell { + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); +} + +.mutedText { + color: var(--monitor-muted); +} + +.statusCell { + display: grid; + gap: 6px; + min-width: 88px; +} + +.recentStatusCell { + display: flex; + align-items: center; + min-width: 70px; +} + +.realtimeTable .statusBadge { + min-height: 24px; + padding: 0 8px; + font-size: 12px; + justify-content: center; + text-align: center; + width: 100%; +} + +.realtimeTable .realtimeReasoningBadge .statusBadge { + width: auto; + min-width: 56px; + max-width: 100%; + padding-inline: 10px; +} + +.realtimeTable .realtimeNonStreamingBadge .statusBadge { + color: var(--monitor-cyan); + border-color: color-mix(in srgb, var(--monitor-cyan) 26%, var(--monitor-line)); + background: color-mix(in srgb, var(--monitor-cyan) 10%, var(--monitor-surface)); +} + +.realtimeTable .statusBadge::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +.accountButton { + width: 100%; + display: grid; + grid-template-columns: 18px minmax(0, 1fr); + gap: 4px 8px; + align-items: start; + border: 0; + background: transparent; + color: inherit; + padding: 0; + min-width: 0; + text-align: left; + cursor: pointer; + white-space: normal; +} + +.accountButton:hover { + transform: none; + color: var(--monitor-accent-strong); +} + +.accountButton:focus, +.accountButton:focus-visible { + outline: none; + box-shadow: none; +} + +.accountButton:focus-visible { + color: var(--monitor-accent-strong); +} + +.accountButtonLabel { + min-width: 0; + font-size: 13px; + font-weight: 600; + line-height: 1.35; + overflow-wrap: anywhere; + word-break: break-word; +} + +.accountButton small { + grid-column: 2; + color: var(--monitor-muted); + font-size: 11px; + line-height: 1.35; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; +} + +.expandedAccountButton { + align-items: start; +} + +.focusedRow td { + border-color: var(--monitor-line); + background: var(--monitor-surface-elevated); + box-shadow: none; +} + +.expandedAccountCardRow td { + padding: 8px 0; + border-top: 1px solid var(--monitor-line); + background: transparent; + white-space: normal; +} + +.expandedAccountCardRow td:first-child, +.expandedAccountCardRow td:last-child { + border-radius: 0; +} + +//.expandedAccountCardRow:hover td { +// background: transparent; +//} + +.expandedAccountCard { + display: grid; + gap: 16px; + min-width: 0; + border-radius: 12px; + border: 1px solid var(--monitor-line); + background: var(--monitor-surface-elevated); +} + +.expandedAccountSummary { + display: grid; + grid-template-columns: var(--account-overview-columns); + gap: 0; + align-items: center; + min-width: 0; +} + +.expandedAccountPrimary, +.expandedAccountBody { + min-width: 0; +} + +.expandedAccountPrimary, +.expandedAccountMetricValue { + display: flex; + align-items: center; + box-sizing: border-box; + padding: 14px 10px; + min-width: 0; +} + +.expandedAccountPrimary { + align-items: flex-start; +} + +.expandedAccountMetricValue strong { + font-size: 12px; + line-height: 1.35; +} + +.expandedAccountAction { + display: flex; + align-items: center; + justify-content: flex-end; + box-sizing: border-box; + padding: 14px 10px; + min-width: 0; +} + +.expandPanel { + display: grid; + gap: 16px; + min-width: 0; +} + +.quotaRefreshButton { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 34px; + padding: 0 12px; + border: 1px solid var(--monitor-line); + border-radius: 999px; + background: var(--monitor-surface); + color: var(--monitor-ink); + font: inherit; + font-size: 12px; + font-weight: 700; + cursor: pointer; + white-space: nowrap; +} + +.quotaRefreshButton:disabled { + cursor: not-allowed; + opacity: 0.72; + transform: none; +} + +.quotaSection { + display: grid; + gap: 12px; + padding: 0; + border: 0; + background: transparent; +} + +.quotaSectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.quotaSectionTitleGroup { + display: grid; + gap: 4px; +} + +.quotaSectionTitleGroup strong { + font-size: 14px; + line-height: 1.2; +} + +.quotaSectionTitleGroup span, +.quotaEntryMain small, +.quotaWindowRow small { + color: var(--monitor-muted); + font-size: 12px; +} + +.quotaEntryGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 12px; +} + +.quotaCompactCard { + display: grid; + gap: 14px; + min-width: 0; + padding: 14px 16px; + border-radius: 16px; + border: 0; + background: color-mix(in srgb, var(--monitor-accent) 6%, var(--monitor-surface-elevated)); +} + +.quotaCompactHeader { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.quotaEntryCard { + display: grid; + gap: 12px; + min-width: 0; + padding: 14px 16px; + border-radius: 14px; + border: 0; + background: var(--monitor-surface-elevated); +} + +.quotaEntryHeader, +.quotaEntryMain, +.quotaWindowList, +.quotaWindowRow { + display: grid; + gap: 8px; +} + +.quotaEntryMain strong, +.quotaWindowHeader strong { + font-size: 13px; + line-height: 1.3; +} + +.quotaWindowHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.quotaWindowHeader span { + color: var(--monitor-ink); + font-size: 12px; +} + +.quotaProgressTrack { + position: relative; + overflow: hidden; + height: 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--monitor-line) 68%, transparent); +} + +.quotaProgressBar { + position: absolute; + inset: 0 auto 0 0; + border-radius: inherit; + background: linear-gradient(135deg, var(--monitor-accent), var(--monitor-cyan)); +} + +.quotaStateMessage { + display: grid; + min-height: 44px; + place-items: center start; + padding: 10px 12px; + border-radius: 14px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); + background: color-mix(in srgb, var(--monitor-surface-elevated) 82%, var(--monitor-surface)); + color: var(--monitor-muted); + font-size: 12px; +} + +.accountSectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; + min-width: 0; +} + +.accountSectionHeader strong { + min-width: 0; + color: var(--monitor-ink); + font-size: 14px; + line-height: 1.25; +} + +.accountOverviewCard .accountButton { + align-items: center; +} + +.accountOverviewCard .accountButtonLabel { + overflow: hidden; + overflow-wrap: normal; + text-overflow: ellipsis; + white-space: nowrap; + word-break: normal; + line-height: 1.45; +} + +.accountOverviewCard .accountButton small { + overflow: hidden; + overflow-wrap: normal; + text-overflow: ellipsis; + white-space: nowrap; + word-break: normal; +} + +.accountOverviewCardBody .quotaRefreshButton { + gap: 6px; + min-height: 28px; + padding: 0 9px; + font-size: 11px; +} + +.accountOverviewCardBody .quotaSection { + align-content: start; + gap: 10px; + min-width: 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; +} + +.accountOverviewCardBody .quotaSectionHeader { + gap: 8px; + flex-wrap: wrap; +} + +.accountOverviewCardBody .quotaEntryGrid { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 10px; +} + +.accountOverviewCardBody .quotaEntryCard { + gap: 10px; + min-width: 0; + overflow: hidden; + padding: 12px; + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); + background: color-mix(in srgb, var(--monitor-surface) 78%, var(--monitor-surface-elevated)); +} + +.accountQuotaContent, +.accountQuotaRow, +.accountQuotaRowHeader, +.accountQuotaModel, +.accountQuotaMeta, +.accountQuotaPlanValue, +.accountQuotaPremiumPlanValue, +.accountQuotaResetCreditRow, +.accountQuotaResetCreditTime { + min-width: 0; + max-width: 100%; +} + +.accountQuotaContent, +.accountQuotaRow { + width: 100%; +} + +.accountQuotaRowHeader { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, max-content); + align-items: baseline; + gap: 8px; +} + +.accountQuotaModel { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accountQuotaMeta { + display: flex; + align-items: baseline; + justify-content: flex-end; + flex-wrap: nowrap; + gap: 8px; + width: auto; + line-height: 1.4; + overflow: hidden; + white-space: nowrap; +} + +.accountQuotaAmount { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.accountQuotaMeta > :not(.accountQuotaAmount) { + flex: 0 0 auto; +} + +.accountQuotaPlanValue { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accountQuotaPremiumPlanValue { + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accountQuotaResetCreditRow { + align-items: flex-start; + flex-wrap: wrap; +} + +.accountQuotaResetCreditTime { + flex: 1 1 140px; + text-align: left; +} + +.realtimeTable .primaryCell small { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + white-space: normal; + overflow-wrap: anywhere; + line-height: 1.45; +} + +.realtimeTokenCell > span, +.realtimeTokenCell > small { + display: block; + white-space: nowrap; + overflow-wrap: normal; + word-break: normal; +} + +.realtimeTable .realtimeTokenCell > small { + display: block; + overflow: visible; + text-overflow: clip; + white-space: nowrap; + -webkit-box-orient: initial; + -webkit-line-clamp: unset; +} + +.logRowFailed td { + border-top-color: color-mix(in srgb, var(--monitor-red) 28%, var(--monitor-line)); + border-bottom-color: color-mix(in srgb, var(--monitor-red) 28%, var(--monitor-line)); +} + +.logRowFailed td:first-child, +.logRowFailed td:last-child { + border-color: color-mix(in srgb, var(--monitor-red) 28%, var(--monitor-line)); +} + +//.logRowFailed:hover td { +// background: color-mix(in srgb, var(--monitor-red) 5%, var(--monitor-surface-hover)); +//} + +.emptyBlock, +.emptyTable, +.emptyBlockSmall { + display: grid; + place-items: center; + color: var(--monitor-muted); + text-align: center; + font-size: 14px; +} + +.emptyBlock, +.emptyTable { + min-height: 120px; +} + +.emptyBlockSmall { + min-height: 80px; +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_responsive.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_responsive.scss new file mode 100644 index 0000000..b21057f --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_responsive.scss @@ -0,0 +1,219 @@ +@media (max-width: 900px) { + .priceManagerModal :global(.modal-body) { + max-height: min(76vh, 680px); + } + + .priceRuleWorkspace { + grid-template-columns: 1fr; + grid-template-rows: 154px minmax(0, 1fr); + height: min(70vh, 620px); + } + + .priceRuleSidebar { + grid-template-rows: auto minmax(0, 1fr); + border-right: 0; + border-bottom: 1px solid var(--monitor-line); + } + + .priceRuleSearch { + padding: 10px 14px; + } + + .priceRuleList { + display: flex; + gap: 7px; + overflow-x: auto; + overflow-y: hidden; + padding: 7px 10px 10px; + } + + .priceRuleListItem { + flex: 0 0 220px; + min-height: 58px; + padding: 8px 10px; + } + + .priceRuleListEmpty { + flex: 1 1 100%; + min-height: 58px; + } + + .priceSyncHeader { + align-items: flex-start; + flex-direction: column; + } + + .priceSyncActions { + width: 100%; + flex-wrap: wrap; + } + + .priceSyncSchedule { + align-items: stretch; + flex-direction: column; + } + + .priceSyncScheduleControls { + width: 100%; + flex-wrap: wrap; + } + + .priceSyncChangesHeader { + align-items: flex-start; + flex-direction: column; + } + + .priceSyncChangesToolbar { + width: 100%; + justify-content: space-between; + } + + .priceSyncChangeFilters { + flex: 1 1 100%; + width: 100%; + max-width: 100%; + overflow-x: auto; + } + + .priceSyncChangeRow { + grid-template-columns: 1fr; + gap: 8px; + } + + .priceSyncRateChanges { + justify-content: flex-start; + } +} + +@media (max-width: 680px) { + .priceManagerModal :global(.modal-body) { + max-height: min(72vh, calc(100dvh - 150px)); + } + + .priceManagerTabs { + padding-inline: 10px 46px; + } + + .priceManagerTab { + min-width: 0; + flex: 1 1 0; + justify-content: center; + padding-inline: 8px; + } + + .priceRuleWorkspace { + grid-template-rows: 150px minmax(0, 1fr); + height: min(66vh, calc(100dvh - 160px)); + } + + .priceRuleEditorHeader, + .priceRuleSection, + .priceRuleEditorFooter, + .priceSyncView { + padding-inline: 14px; + } + + .priceRuleEditorHeader { + min-height: 66px; + } + + .priceRuleEditorBadges > span:not(:first-child) { + display: none; + } + + .priceBaseGrid { + grid-template-columns: 1fr; + gap: 10px; + } + + .priceTierCompactRow { + position: relative; + grid-template-columns: repeat(2, minmax(0, 1fr)); + padding: 34px 10px 10px; + } + + .priceTierCompactRow label:first-of-type { + grid-column: 1 / -1; + } + + .priceTierIndex { + position: absolute; + top: 8px; + left: 10px; + width: 24px; + height: 20px; + } + + .priceTierRemoveButton { + position: absolute; + top: 6px; + right: 8px; + width: 30px; + height: 26px; + } + + .priceRuleEditorFooter { + align-items: stretch; + flex-direction: column-reverse; + } + + .priceRuleEditorFooter > div { + width: 100%; + } + + .priceRuleEditorFooter > div:last-child :global(.btn) { + flex: 1 1 0; + } + + .priceSyncView { + padding-block: 18px; + } + + .priceSyncActions :global(.btn) { + flex: 1 1 132px; + } + + .priceSyncMetrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-height: 158px; + } + + .priceSyncMetrics > div:nth-child(2) { + border-right: 0; + } + + .priceSyncMetrics > div:nth-child(n + 3) { + border-top: 1px solid var(--monitor-line); + } + + .priceSyncOverrideAll { + width: 100%; + justify-content: center; + } + + .priceSyncChangeFilters button { + flex: 0 0 auto; + } + + .priceSyncScheduleControls { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: end; + } + + .priceSyncScheduleToggle { + grid-column: 1 / -1; + } + + .priceSyncScheduleInterval { + width: 100%; + } + + .priceSyncSchedule :global(.btn) { + min-height: 34px; + } + + .priceUnmatchedList > div:not(.priceTierEmpty) { + align-items: flex-start; + } +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_utilities.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_utilities.scss new file mode 100644 index 0000000..f055fb8 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/styles/_utilities.scss @@ -0,0 +1,651 @@ +@keyframes monitor-spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +@keyframes monitor-pulse { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.45; + } +} + +@container monitoring-page (max-width: 760px) { + .usageStatsGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@container monitoring-page (max-width: 1120px) { + .toolbarControlRow { + flex-wrap: wrap; + justify-content: flex-start; + } + + .refreshCluster { + justify-content: flex-start; + } +} + +@container monitoring-page (max-width: 900px) { + .usageTrendHeader, + .usageTrendCollapsed { + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + gap: 10px 12px; + } + + .usageTrendCopy { + display: contents; + } + + .usageTrendCopy h2 { + grid-column: 1; + grid-row: 1; + align-self: center; + } + + .usageTrendHeader .usageTrendCopy p { + grid-column: 1 / -1; + grid-row: 2; + } + + .usageTrendActions { + grid-column: 1 / -1; + grid-row: 3; + width: 100%; + justify-content: flex-start; + } + + .usageTrendHeader > .usageTrendActions > .usageTrendHideButton { + display: none; + } + + .mobileHeaderHideButton { + display: inline-flex; + grid-column: 2; + align-self: center; + margin-top: 0; + transform: translateY(5px); + } + + .timeRangeControl, + .usageTrendRangeControl { + flex: 0 1 auto; + } + + .usageTrendApiKeySelect { + width: 150px; + } + + .usageTrendInsightsGrid, + .rankingGrid { + grid-template-columns: 1fr; + } + + .modelStatsLayout { + grid-template-columns: minmax(0, 1fr) minmax(210px, 240px); + } + + .settingsGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .accountOverviewTable { + min-width: 920px; + } + + .realtimeTable { + min-width: 1360px; + } + + .accountOverviewTable th:first-child, + .accountOverviewTable td:first-child { + position: sticky; + left: 0; + z-index: 2; + box-shadow: 8px 0 14px color-mix(in srgb, var(--monitor-bg) 56%, transparent); + } + + .accountOverviewTable th:first-child { + z-index: 4; + background: var(--monitor-surface); + } + + .accountOverviewTable td:first-child { + background: var(--monitor-surface-elevated); + } + +} + +@container monitoring-page (max-width: 680px) { + .realtimeLogStatusRow { + align-items: flex-start; + flex-direction: column; + } + + .realtimeUpdateBar { + align-items: flex-start; + flex-wrap: wrap; + } + + .usageTrendActions { + align-items: center; + overflow: visible; + } + + .timeRangeControl, + .usageTrendRangeControl { + flex: 0 1 auto; + min-width: 0; + width: auto; + overflow-x: auto; + scrollbar-width: none; + } + + .timeRangeControl::-webkit-scrollbar, + .usageTrendRangeControl::-webkit-scrollbar { + display: none; + } + + .timeRangeButton, + .usageTrendRangeButton { + flex: 0 0 auto; + min-width: 54px; + padding: 0 10px; + } + + .usageTrendApiKeySelect { + width: 132px; + } + + .usageTrendChartCard { + height: auto; + min-height: 520px; + padding: 16px; + } + + .professionalChartShell { + grid-template-rows: auto minmax(230px, 240px) auto; + gap: 16px; + align-content: start; + } + + .usageTrendSvg { + height: 240px; + } + + .trendChartLegend { + justify-content: flex-start; + } + + .accountStatsToolbar { + align-items: stretch; + flex-direction: column; + flex-wrap: nowrap; + } + + .accountStatsFilters { + display: grid; + grid-template-columns: minmax(112px, 1fr) minmax(104px, 0.9fr) minmax(96px, 0.8fr); + gap: 8px; + overflow-x: auto; + flex-wrap: nowrap; + scrollbar-width: none; + } + + .accountStatsFilters::-webkit-scrollbar { + display: none; + } + + .accountStatsSearchInput, + .accountStatsSelect { + width: 100%; + } + + .accountStatsToolbar > .rankingMetricSwitch { + width: max-content; + max-width: 100%; + overflow-x: auto; + justify-content: flex-start; + scrollbar-width: none; + } + + .accountStatsToolbar > .rankingMetricSwitch::-webkit-scrollbar { + display: none; + } + + .accountStatsToolbar > .rankingMetricSwitch .rankingMetricButton { + flex: 0 0 auto; + min-width: 58px; + padding: 0 12px; + } + + .accountStatsClearButton { + display: none; + } + + .accountOverviewCardGrid { + grid-template-columns: 1fr; + } + + .healthMetricGrid, + .accountOverviewMetricGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .trendCardHeader { + flex-direction: column; + } + + .trendSummaryStrip { + grid-template-columns: repeat(4, minmax(96px, 1fr)); + overflow-x: auto; + scrollbar-width: none; + } + + .trendSummaryStrip::-webkit-scrollbar { + display: none; + } + + .tokenDistributionLegend { + justify-content: flex-start; + } + + .trendMetricStrip, + .tokenTypeSummaryGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .modelStatsLayout { + grid-template-columns: minmax(0, 1fr) minmax(190px, 220px); + } + + .modelStatsCard .modelSharePanel { + grid-template-columns: 1fr; + justify-items: center; + gap: 8px; + } + + .modelStatsCard .modelShareHeader, + .modelStatsCard .modelLegend, + .modelStatsCard .donutChart { + grid-column: auto; + grid-row: auto; + } + + .modelStatsCard .donutChart { + width: min(150px, 100%); + } +} + +@container monitoring-page (max-width: 520px) { + .usageStatsGrid { + grid-template-columns: 1fr; + } + + .rankingHeader { + flex-wrap: nowrap; + } + + .rankingMetricSwitch { + justify-content: flex-start; + max-width: 100%; + } + + .rankingMetricButton { + padding: 0 9px; + } + + .tokenDistributionHeader { + flex-direction: row; + align-items: flex-start; + } + + .tokenCostBadge { + min-width: 98px; + padding: 7px 9px; + } + + .trendSummaryStrip { + grid-template-columns: repeat(4, minmax(92px, 1fr)); + } + + .usageTrendChartCard { + min-height: 500px; + padding: 12px; + } + + .professionalChartShell { + grid-template-rows: auto minmax(210px, 220px) auto; + gap: 16px; + } + + .usageTrendSvg { + height: 220px; + } + + .trendSummaryItem { + padding: 10px 10px 10px 16px; + } + + .trendSummaryItem strong { + font-size: 17px; + } + + .trendChartLegend { + gap: 8px 14px; + } + + .trendChartLegend span { + font-size: 12px; + } + + .trendChartLegend span::before { + width: 18px; + } + + .modelStatsLayout { + grid-template-columns: minmax(0, 1fr) minmax(160px, 190px); + gap: 12px; + } + + .modelStatsCard .modelSharePanel { + grid-template-columns: 1fr; + justify-items: center; + gap: 8px; + } + + .modelStatsCard .modelShareHeader, + .modelStatsCard .modelLegend, + .modelStatsCard .donutChart { + grid-column: auto; + grid-row: auto; + } + + .modelStatsCard .donutChart { + width: min(140px, 100%); + } + + .modelStatsCard .modelLegend { + gap: 4px; + } + + .modelSharePanel .donutTooltip { + right: auto; + bottom: auto; + left: 50%; + top: calc(100% + 12px); + transform: translate(-50%, 0); + } + + .modelSharePanel .donutChart:hover .donutTooltip, + .modelSharePanel .donutChart:focus-within .donutTooltip { + transform: translate(-50%, 0); + } +} + +@container monitoring-page (max-width: 768px) { + .masthead, + .panel { + border-radius: 12px; + } + + .panel { + padding: 16px; + } + + .masthead { + gap: 16px; + } + + .titleRow { + grid-template-columns: 1fr; + } + + .titleActions { + justify-content: flex-start; + width: 100%; + } + + .title { + font-size: 28px; + } + + .subtitle { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + font-size: 12px; + line-height: 1.5; + } + + .mastheadActionButton { + flex: 0 0 auto; + } + + .toolbarControlRow { + width: 100%; + flex-direction: column; + align-items: stretch; + } + + .segmentedControl, + .refreshCluster { + width: 100%; + max-width: 100%; + min-width: 0; + justify-content: flex-start; + } + + .refreshCluster { + flex-wrap: wrap; + } + + .refreshControls { + min-width: 0; + flex: 0 1 auto; + } + + .segmentedControl { + flex: 0 0 auto; + } + + .quickLinkButton { + flex: 0 0 auto; + } + + .realtimePanel .panelHeader { + flex-direction: column; + align-items: stretch; + gap: 10px; + } + + .realtimePanel .panelTitle { + flex: 0 1 auto; + min-width: 0; + overflow: hidden; + font-size: 16px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .realtimePanel .panelExtra { + flex: 1 1 auto; + min-width: 0; + width: 100%; + } + + .toolbarHeaderActions { + width: 100%; + } + + .toolbarHeaderActions .clearButton { + flex: 0 0 auto; + } + + .autoRefreshField { + flex: 1 1 160px; + min-width: 0; + width: auto; + justify-content: flex-start; + } + + .autoRefreshSelect { + width: 76px; + } + + .toolbarHeaderActions { + flex-wrap: nowrap; + } + + .toolbarHeaderSearchInput { + min-width: 0; + } + + .refreshCluster { + overflow-x: visible; + scrollbar-width: none; + } + + .refreshCluster::-webkit-scrollbar { + display: none; + } + + .syncPill { + min-width: 0; + max-width: 112px; + overflow: hidden; + padding: 0 8px; + font-size: 11px; + text-overflow: ellipsis; + } + + .autoRefreshLabel { + font-size: 10px; + } + + .autoRefreshLabel svg { + display: none; + } + + .autoRefreshSelectTrigger { + min-width: 68px; + } + + .refreshButtonLabel { + display: none; + } + + .refreshButton { + min-width: 42px; + } + + .usageStatsCard { + padding: 13px; + } + + .usageStatsBody strong { + font-size: 22px; + } + + .usageStatsFooter { + gap: 6px; + } + + .filterGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .rankingGrid { + grid-template-columns: 1fr; + } + + .filterGrid :global(.select-trigger) { + padding-inline: 8px !important; + } + + .filterGrid :global(.select-value) { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .settingsGrid { + grid-template-columns: 1fr; + } + + .settingsDangerAction { + align-items: stretch; + flex-direction: column; + } + + .settingsDangerAction :global(.btn) { + width: 100%; + } + + .segmentButton { + min-height: 32px; + padding: 0 10px; + font-size: 12px; + } + + .refreshCluster { + gap: 8px; + padding: 6px; + } + + .refreshControls { + flex: 1 1 auto; + justify-content: flex-end; + } + + .autoRefreshField { + flex-basis: 132px; + } + + .syncPill, + .autoRefreshField, + .refreshButton { + min-height: 38px; + } + + .refreshButton { + min-width: 42px; + padding: 0 10px; + } + + .filterGrid { + gap: 8px; + } + + .priceActionsBar { + justify-content: stretch; + } + + .priceActionsBar :global(.btn) { + flex: 1 1 0; + } + + .table { + min-width: 0; + } + + .accountOverviewTable { + min-width: 920px; + } + + .realtimeTable { + min-width: 1360px; + } +} diff --git a/cliproxyapi-pro-management/overlay/src/pages/MonitoringCenterPage.module.scss b/cliproxyapi-pro-management/overlay/src/pages/MonitoringCenterPage.module.scss deleted file mode 100644 index bfe8793..0000000 --- a/cliproxyapi-pro-management/overlay/src/pages/MonitoringCenterPage.module.scss +++ /dev/null @@ -1,6213 +0,0 @@ -.page, -.monitorModal { - --monitor-bg: var(--bg-secondary); - --monitor-surface: var(--bg-primary); - --monitor-surface-elevated: color-mix(in srgb, var(--bg-primary) 88%, var(--bg-secondary)); - --monitor-surface-muted: var(--bg-secondary); - --monitor-surface-hover: var(--bg-tertiary); - --monitor-ink: var(--text-primary); - --monitor-muted: var(--text-secondary); - --monitor-line: var(--border-color); - --monitor-line-strong: var(--border-primary); - --monitor-accent: var(--primary-color); - --monitor-accent-strong: var(--primary-color); - --monitor-accent-soft: rgba(59, 130, 246, 0.12); - --monitor-cyan: #06b6d4; - --monitor-green: var(--success-color); - --monitor-amber: #f59e0b; - --monitor-red: var(--error-color); - color: var(--monitor-ink); -} - -.page { - container: monitoring-page / inline-size; - display: grid; - gap: 24px; - min-width: 0; -} - -.masthead, -.panel { - position: relative; - overflow: hidden; - border: 1px solid var(--monitor-line); - border-radius: 12px; - background: var(--monitor-surface); - box-shadow: var(--shadow); -} - -.masthead { - display: block; - padding: 0; - border: 0; - border-radius: 0; - background: transparent; - box-shadow: none; -} - -.mastheadGlow { - display: none; -} - -.mastheadCopy { - display: grid; - gap: 14px; - min-width: 0; -} - -.titleRow { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: start; - gap: 20px; - min-width: 0; -} - -.title { - margin: 0; - min-width: 0; - font-size: 28px; - font-weight: 700; - line-height: 1.15; - letter-spacing: 0; -} - -.titleActions { - display: inline-flex; - flex-wrap: nowrap; - align-items: center; - justify-content: flex-end; - gap: 8px; - min-width: 0; - max-width: 100%; - overflow-x: auto; - padding: 4px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); - border-radius: 14px; - background: color-mix(in srgb, var(--monitor-surface) 74%, transparent); - box-shadow: 0 12px 28px -26px rgba(15, 23, 42, 0.5); - scrollbar-width: none; -} - -.titleActions::-webkit-scrollbar { - display: none; -} - -.hiddenFileInput { - display: none; -} - -.subtitle { - margin: 0; - max-width: 760px; - color: var(--monitor-muted); - font-size: 13px; - line-height: 1.55; -} - -.mastheadActionButton { - flex: 0 0 auto; - align-self: center; - min-height: 36px; - padding: 0 14px; - border-radius: 10px; - border-color: transparent; - background: transparent; - color: var(--monitor-muted); - font-size: 13px; - font-weight: 700; -} - -.mastheadActionButton:hover { - border-color: color-mix(in srgb, var(--monitor-accent) 20%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-accent) 8%, var(--monitor-surface)); - color: var(--monitor-ink); - box-shadow: none; -} - -.segmentedControl { - display: inline-flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 4px; - padding: 3px; - border-radius: 999px; - border: 1px solid var(--monitor-line); - background: var(--monitor-surface-muted); -} - -.segmentButton, -.refreshButton, -.quickLinkButton, -.clearButton, -.inlineActionButton, -.quotaRefreshButton, -.accountButton, -.statusBadge { - transition: - transform 0.18s ease, - border-color 0.18s ease, - background-color 0.18s ease, - color 0.18s ease, - box-shadow 0.18s ease; -} - -.segmentButton { - min-height: 32px; - padding: 0 12px; - border: 0; - border-radius: 999px; - background: transparent; - color: var(--monitor-muted); - font-size: 12px; - font-weight: 700; - cursor: pointer; -} - -.segmentButton:hover { - transform: translateY(-1px); - color: var(--monitor-ink); -} - -.segmentButtonActive { - background: var(--monitor-accent); - color: var(--primary-contrast, #fff); - box-shadow: 0 8px 18px -14px rgba(0, 0, 0, 0.45); -} - -.refreshCluster { - display: flex; - flex-wrap: nowrap; - align-items: center; - justify-content: flex-end; - gap: 6px; - padding: 4px; - border-radius: 10px; - border: 1px solid var(--monitor-line); - background: var(--monitor-surface-muted); -} - -.refreshControls { - display: inline-flex; - align-items: center; - gap: 8px; - flex-wrap: nowrap; -} - -.syncPill, -.quickLinkButton, -.clearButton, -.inlineActionButton, -.statusBadge { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 34px; - padding: 0 10px; - border-radius: 999px; - border: 1px solid var(--monitor-line); - background: var(--monitor-surface-elevated); -} - -.syncPill, -.quickLinkButton { - color: var(--monitor-muted); - font-size: 12px; - white-space: nowrap; -} - -.syncPill { - min-height: 36px; - padding: 0 10px; -} - -.syncPill.tonegood { - border-color: color-mix(in srgb, var(--monitor-green) 30%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-green) 9%, var(--monitor-surface)); - color: var(--monitor-green); -} - -.syncPill.tonewarn { - border-color: color-mix(in srgb, var(--monitor-amber) 30%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-amber) 9%, var(--monitor-surface)); - color: var(--monitor-amber); -} - -.syncPill.tonebad { - border-color: color-mix(in srgb, var(--monitor-red) 30%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-red) 8%, var(--monitor-surface)); - color: var(--monitor-red); -} - -.quickLinkButton { - gap: 5px; - min-height: 32px; - padding: 0 10px; - font-size: 11px; -} - -.quickLinkButton { - text-decoration: none; - cursor: pointer; - font: inherit; - border-radius: 8px; -} - -.quickLinkButton:hover, -.clearButton:hover, -.inlineActionButton:hover, -.quotaRefreshButton:hover, -.refreshButton:hover { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--monitor-accent) 35%, var(--monitor-line-strong)); - background: var(--monitor-surface-hover); - color: var(--monitor-ink); -} - -.clearButton, -.inlineActionButton { - gap: 8px; - color: var(--monitor-ink); - font-size: 12px; - font-weight: 700; - cursor: pointer; -} - -.autoRefreshField { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 36px; - padding: 0 6px 0 10px; - border-radius: 12px; - border: 1px solid var(--monitor-line); - background: var(--monitor-surface); -} - -.autoRefreshLabel { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--monitor-muted); - font-size: 11px; - font-weight: 600; - white-space: nowrap; -} - -.autoRefreshSelect { - width: 94px; - flex: 0 0 auto; -} - -.autoRefreshSelectTrigger { - min-width: 76px; - height: 30px !important; - padding: 0 2px 0 6px !important; - border: 0 !important; - border-radius: 10px !important; - background: transparent !important; - box-shadow: none !important; - color: var(--monitor-ink); - font-size: 12px; - font-weight: 600; - line-height: 1; -} - -.autoRefreshSelectTrigger:hover, -.autoRefreshSelectTrigger:focus, -.autoRefreshSelectTrigger[aria-expanded='true'] { - border: 0 !important; - background: transparent !important; - box-shadow: none !important; -} - -.refreshButton { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - min-width: 92px; - min-height: 36px; - padding: 0 12px; - border: 1px solid var(--monitor-line); - border-radius: 12px; - background: var(--monitor-surface); - color: var(--monitor-ink); - font: inherit; - font-size: 12px; - font-weight: 700; - cursor: pointer; - white-space: nowrap; -} - -.refreshButton:disabled { - cursor: not-allowed; - opacity: 0.72; - transform: none; -} - -.refreshIcon, -.refreshIconSpinning { - display: block; - flex-shrink: 0; - transform-origin: center; -} - -.refreshIconSpinning { - animation: monitor-spin 0.9s linear infinite; -} - -.refreshButtonLabel { - display: inline-flex; - align-items: center; - line-height: 1; -} - -.panel { - min-width: 0; - padding: 24px; -} - -.panelHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 14px; - margin-bottom: 16px; -} - -.panelTitle { - margin: 0; - font-size: 19px; - font-weight: 700; - letter-spacing: -0.02em; -} - -.panelSubtitle { - margin: 6px 0 0; - color: var(--monitor-muted); - font-size: 13px; - line-height: 1.6; -} - -.panelExtra { - flex-shrink: 0; -} - -.toolbarHeaderActions { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 8px; - width: min(420px, 44vw); - min-width: 0; -} - -.toolbarHeaderActions :global(.form-group) { - flex: 1 1 auto; - min-width: 0; - margin: 0; -} - -.toolbarHeaderActions :global(.form-group > div) { - display: flex; - align-items: center; -} - -.toolbarHeaderSearchInput { - width: 100%; - min-width: 0; - height: 40px; - min-height: 40px; - padding-block: 0; - padding-right: 36px; - border-radius: 8px; - font-size: 13px; - line-height: 1; -} - -.toolbarHeaderActions .clearButton { - flex: 0 0 auto; - min-height: 34px; - padding: 0 10px; - font-size: 11px; -} - -.pricingPanel, -.accountPanel { - background: var(--monitor-surface); -} - -.realtimePanel { - display: grid; - gap: 14px; - min-width: 0; - padding: 18px; - border: 1px solid var(--monitor-line); - border-radius: 12px; - background: var(--monitor-surface); - box-shadow: var(--shadow); -} - -.accountPanelActions { - min-width: 180px; -} - -.toolbarControlRow { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 14px; - padding: 12px 14px; - border-radius: 10px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 60%, transparent); - background: color-mix(in srgb, var(--monitor-surface-muted) 60%, transparent); -} - -.filterGrid { - display: grid; - grid-template-columns: minmax(260px, 1.4fr) repeat(4, minmax(128px, 1fr)) auto auto; - gap: 10px; - align-items: center; -} - -.filterGrid :global(.form-group) { - min-width: 0; - margin: 0; -} - -.filterGrid .clearButton { - min-height: 40px; - padding: 0 12px; - white-space: nowrap; -} - -.inlineMetrics { - display: flex; - flex: 1 1 auto; - flex-wrap: nowrap; - gap: 6px; - min-width: 0; - max-width: 100%; - overflow-x: auto; - scrollbar-width: none; -} - -.inlineMetrics::-webkit-scrollbar { - display: none; -} - -.inlineMetrics span { - display: inline-flex; - align-items: center; - min-height: 30px; - padding: 0 10px; - border-radius: 999px; - border: 1px solid var(--monitor-line); - background: var(--monitor-surface-elevated); - color: var(--monitor-muted); - font-size: 12px; - white-space: nowrap; - flex: 0 0 auto; -} - -.realtimeLogStatusRow { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - min-width: 0; -} - -.realtimeFollowToggle { - position: relative; - display: inline-flex; - align-items: center; - gap: 8px; - flex: 0 0 auto; - min-height: 30px; - color: var(--monitor-muted); - font-size: 12px; - cursor: pointer; -} - -.realtimeFollowToggle input { - position: absolute; - width: 1px; - height: 1px; - opacity: 0; - pointer-events: none; -} - -.realtimeFollowTrack { - position: relative; - display: inline-flex; - width: 34px; - height: 18px; - flex: 0 0 auto; - border: 1px solid color-mix(in srgb, var(--monitor-muted) 44%, var(--monitor-line)); - border-radius: 999px; - background: var(--monitor-surface-muted); - transition: border-color 0.16s ease, background 0.16s ease; -} - -.realtimeFollowTrack > span { - position: absolute; - top: 2px; - left: 2px; - width: 12px; - height: 12px; - border-radius: 50%; - background: var(--monitor-muted); - transition: transform 0.16s ease, background 0.16s ease; -} - -.realtimeFollowToggle input:checked + .realtimeFollowTrack { - border-color: var(--monitor-accent-strong); - background: color-mix(in srgb, var(--monitor-accent) 28%, var(--monitor-surface)); -} - -.realtimeFollowToggle input:checked + .realtimeFollowTrack > span { - transform: translateX(16px); - background: var(--monitor-accent-strong); -} - -.realtimeFollowToggle input:focus-visible + .realtimeFollowTrack { - outline: 2px solid color-mix(in srgb, var(--monitor-accent-strong) 45%, transparent); - outline-offset: 2px; -} - -.realtimeFollowLabel { - color: var(--monitor-ink); - font-weight: 650; -} - -.realtimeUpdateBar { - position: absolute; - top: 50px; - right: 14px; - z-index: 6; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - width: min(460px, calc(100% - 28px)); - min-width: 0; - padding: 9px 11px; - border: 1px solid color-mix(in srgb, var(--monitor-accent) 30%, var(--monitor-line)); - border-radius: 8px; - background: color-mix(in srgb, var(--monitor-accent) 7%, var(--monitor-surface)); - box-shadow: var(--shadow); -} - -.realtimeUpdateCopy { - display: grid; - gap: 2px; - min-width: 0; -} - -.realtimeUpdateCopy strong { - color: var(--monitor-ink); - font-size: 12px; - line-height: 1.35; -} - -.realtimeUpdateCopy span { - color: var(--monitor-muted); - font-size: 11px; - line-height: 1.35; -} - -.realtimeUpdateBar .inlineActionButton { - flex: 0 0 auto; -} - -.realtimeTableShell { - position: relative; - min-width: 0; - height: min(620px, 68vh); -} - -.realtimeColumnsMenu { - position: relative; - min-width: 0; -} - -.realtimeColumnsDropdown { - position: absolute; - top: calc(100% + 8px); - right: 0; - z-index: 10; - display: grid; - gap: 10px; - width: max-content; - min-width: 128px; - max-width: min(220px, calc(100vw - 32px)); - padding: 10px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); - border-radius: 8px; - background: var(--monitor-surface); - box-shadow: var(--shadow); -} - -.realtimeColumnsDropdownHeader { - display: grid; - gap: 8px; - color: var(--monitor-muted); - font-size: 12px; -} - -.realtimeColumnsDropdownHeader .inlineActionButton { - justify-self: start; -} - -.realtimeColumnsDropdownList { - display: grid; - gap: 4px; -} - -.realtimeColumnToggle { - display: inline-flex; - align-items: center; - gap: 8px; - min-width: 0; - min-height: 30px; - padding: 4px 6px; - border-radius: 6px; - color: var(--monitor-ink); - font-size: 12px; -} - -.realtimeColumnToggle:hover { - background: color-mix(in srgb, var(--monitor-accent) 8%, transparent); -} - -.realtimeColumnToggle input { - width: 14px; - height: 14px; - margin: 0; - accent-color: var(--monitor-accent-strong); -} - -.realtimeColumnToggle span { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.errorBox, -.callout { - display: grid; - gap: 6px; - padding: 14px 16px; - border-radius: 18px; - border: 1px solid var(--monitor-line); -} - -.errorBox { - margin-top: 14px; - border-color: color-mix(in srgb, var(--monitor-red) 28%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-red) 10%, var(--monitor-surface)); - color: var(--monitor-red); -} - -.callout { - margin-top: 14px; - border-color: color-mix(in srgb, var(--monitor-accent) 24%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-accent) 8%, var(--monitor-surface)); -} - -.usageStatsHero { - display: grid; - gap: 0; - min-width: 0; - padding-top: 2px; -} - -.usageStatsGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; -} - -.usageStatsCard { - display: grid; - gap: 13px; - min-width: 0; - padding: 15px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 86%, transparent); - border-radius: 14px; - background: - radial-gradient(circle at 90% 10%, color-mix(in srgb, var(--usage-accent, var(--monitor-accent)) 10%, transparent), transparent 34%), - color-mix(in srgb, var(--monitor-surface) 96%, var(--monitor-bg)); - box-shadow: 0 14px 34px -30px rgba(15, 23, 42, 0.72); -} - -.usageStatsCard:nth-child(1) { - --usage-accent: #2563eb; -} - -.usageStatsCard:nth-child(2) { - --usage-accent: #7c3aed; -} - -.usageStatsCard:nth-child(3) { - --usage-accent: #16a34a; -} - -.usageStatsCard:nth-child(4) { - --usage-accent: #d97706; -} - -.usageStatsCard:hover { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--usage-accent) 30%, var(--monitor-line)); -} - -.usageStatsCard { - transition: - transform 0.18s ease, - border-color 0.18s ease, - box-shadow 0.18s ease; -} - -.usageStatsCard:hover { - box-shadow: 0 16px 34px -28px var(--usage-accent); -} - -.usageStatsCardHeader { - display: flex; - align-items: center; - gap: 10px; - color: var(--monitor-ink); - font-size: 15px; - font-weight: 700; -} - -.usageStatsIcon { - width: 30px; - height: 30px; - border-radius: 10px; - background: var(--usage-accent-soft); - color: var(--usage-accent); - position: relative; - flex: 0 0 auto; -} - -.usageStatsIcon::before, -.usageStatsIcon::after { - content: ''; - position: absolute; - border-radius: 999px; - background: currentColor; -} - -.usageStatsIcon::before { - left: 8px; - right: 8px; - bottom: 7px; - height: 4px; -} - -.usageStatsIcon::after { - width: 5px; - height: 12px; - right: 9px; - bottom: 10px; - box-shadow: -7px 4px 0 currentColor; -} - -.usageStatsIconblue { - --usage-accent: #2563eb; - --usage-accent-soft: rgba(37, 99, 235, 0.12); -} - -.usageStatsIconpurple { - --usage-accent: #7c3aed; - --usage-accent-soft: rgba(124, 58, 237, 0.12); -} - -.usageStatsIcongreen { - --usage-accent: #16a34a; - --usage-accent-soft: rgba(22, 163, 74, 0.12); -} - -.usageStatsIconamber { - --usage-accent: #d97706; - --usage-accent-soft: rgba(217, 119, 6, 0.12); -} - -.usageStatsBody { - display: grid; - gap: 8px; -} - -.usageStatsBody span, -.usageStatsFooter span { - color: var(--monitor-muted); - font-size: 12px; - line-height: 1.35; -} - -.usageStatsBody strong { - min-width: 0; - overflow: hidden; - color: var(--monitor-ink); - font-size: 24px; - font-weight: 800; - letter-spacing: -0.03em; - line-height: 1; - text-overflow: ellipsis; - white-space: nowrap; -} - -.usageStatsFooter { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; - padding-top: 12px; - border-top: 1px solid var(--monitor-line); -} - -.usageStatsFooter div { - display: grid; - gap: 4px; - min-width: 0; -} - -.usageStatsFooter strong { - min-width: 0; - overflow: hidden; - color: var(--monitor-ink); - font-size: 13px; - line-height: 1.25; - text-overflow: ellipsis; - white-space: nowrap; -} - -.usageStatsCardTokens .usageStatsFooter { - grid-template-columns: minmax(0, 0.68fr) minmax(0, 1.32fr); - column-gap: 6px; -} - -.usageTrendSection { - display: grid; - gap: 16px; - min-width: 0; - overflow: visible; - position: relative; - z-index: 3; -} - -.usageTrendHeader, -.usageTrendCollapsed { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: 18px; - min-width: 0; -} - -.usageTrendCopy { - display: grid; - gap: 8px; - min-width: 0; -} - -.usageTrendCopy h2, -.usageTrendCollapsed h2 { - margin: 0; - color: var(--monitor-ink); - font-size: 22px; - font-weight: 800; - letter-spacing: -0.03em; -} - -.usageTrendCopy p, -.usageTrendCollapsed p { - margin: 0; - color: var(--monitor-muted); - font-size: 13px; - line-height: 1.55; -} - -.usageTrendActions { - display: flex; - flex-wrap: nowrap; - align-items: center; - justify-content: flex-end; - gap: 8px; - max-width: 100%; - min-width: 0; -} - -.timeRangeControl, -.usageTrendRangeControl { - display: flex; - flex-wrap: nowrap; - max-width: 100%; - overflow-x: auto; - scrollbar-width: none; -} - -.timeRangeControl::-webkit-scrollbar, -.usageTrendRangeControl::-webkit-scrollbar { - display: none; -} - -.timeRangeButton, -.usageTrendRangeButton { - flex: 0 0 auto; - min-width: 50px; - padding: 0 12px; -} - -.usageTrendHideButton { - flex: 0 0 auto; - min-height: 36px; - padding: 0 14px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 80%, transparent); - background: color-mix(in srgb, var(--monitor-surface-muted) 78%, var(--monitor-surface)); -} - -.usageTrendHideButton:hover { - color: var(--monitor-ink); -} - -.mobileHeaderHideButton { - display: none; -} - -.usageTrendCollapsed { - padding: 18px 20px; - border: 1px dashed color-mix(in srgb, var(--monitor-line) 82%, transparent); - border-radius: 14px; - background: color-mix(in srgb, var(--monitor-surface) 72%, transparent); -} - -.usageTrendInsightsGrid { - display: grid; - grid-template-columns: minmax(0, 1.22fr) minmax(360px, 0.78fr); - gap: 16px; - align-items: stretch; - --ranking-card-height: 500px; -} - -.usageTrendChartCard { - display: grid; - grid-template-rows: auto minmax(0, 1fr); - gap: 16px; - min-width: 0; - height: 500px; - padding: 20px; - border: 1px solid var(--monitor-line); - border-radius: 12px; - background: var(--monitor-surface); - box-shadow: var(--shadow); - overflow: hidden; -} - -.tokenDistributionCard { - overflow: visible; -} - -.usageTrendLineCard, -.tokenDistributionCard { - background: var(--monitor-surface); -} - -.trendCardHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - min-width: 0; -} - -.trendCardHeader > div:first-child { - min-width: 0; -} - -.trendCardHeader h3 { - margin: 0; - color: var(--monitor-ink); - font-size: 18px; - font-weight: 700; - letter-spacing: -0.02em; -} - -.trendCardHeader p { - margin: 6px 0 0; - color: var(--monitor-muted); - font-size: 13px; - line-height: 1.5; -} - -.usageTrendApiKeySelect { - width: 150px; - flex: 0 0 auto; -} - -.tokenDistributionHeader { - align-items: flex-start; -} - -.tokenCostBadge { - display: grid; - gap: 3px; - flex: 0 0 auto; - min-width: 108px; - padding: 8px 10px; - border: 1px solid color-mix(in srgb, #047857 26%, var(--monitor-line)); - border-radius: 10px; - background: color-mix(in srgb, #047857 8%, var(--monitor-surface)); - text-align: right; -} - -.tokenCostBadge span { - color: var(--monitor-muted); - font-size: 11px; - font-weight: 700; - line-height: 1.1; -} - -.tokenCostBadge strong { - color: #047857; - font-size: 14px; - font-weight: 850; - letter-spacing: -0.01em; - line-height: 1.15; -} - -.trendHeaderStats { - display: grid; - grid-template-columns: repeat(3, minmax(64px, auto)); - gap: 8px; - flex: 0 0 auto; -} - -.trendHeaderStats div { - display: grid; - gap: 3px; - min-width: 0; - padding: 8px 10px; - border-radius: 10px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); - background: color-mix(in srgb, var(--monitor-surface-muted) 48%, var(--monitor-surface)); -} - -.trendHeaderStats span { - color: var(--monitor-muted); - font-size: 11px; - font-weight: 700; - line-height: 1.1; -} - -.trendHeaderStats strong { - color: var(--monitor-ink); - font-size: 13px; - font-weight: 800; - letter-spacing: -0.01em; - line-height: 1.15; -} - -.professionalChartShell { - display: grid; - grid-template-rows: auto minmax(250px, 260px) auto; - gap: 16px; - min-width: 0; - min-height: 0; - align-content: start; -} - -.trendChartLegend { - display: inline-flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 8px 18px; - min-width: 0; - color: var(--monitor-ink); -} - -.trendChartLegend span { - display: inline-flex; - align-items: center; - gap: 7px; - color: color-mix(in srgb, var(--monitor-ink) 64%, var(--monitor-muted)); - font-size: 13px; - font-weight: 750; - letter-spacing: -0.01em; - line-height: 1; - white-space: nowrap; -} - -.trendChartLegend span::before { - content: ''; - width: 22px; - height: 3px; - border-radius: 999px; - background: var(--series-color); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--series-color) 12%, transparent); -} - -.trendSummaryStrip { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.trendSummaryItem { - position: relative; - display: grid; - gap: 6px; - min-width: 0; - padding: 12px 12px 12px 18px; - overflow: hidden; - border-radius: 14px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 84%, transparent); - background: color-mix(in srgb, var(--monitor-surface) 96%, var(--monitor-bg)); - box-shadow: 0 10px 24px -26px rgba(15, 23, 42, 0.58); -} - -.trendSummaryItem::before { - content: ''; - position: absolute; - inset: 10px auto 10px 0; - width: 4px; - border-radius: 999px; - background: var(--series-color); -} - -.trendSummaryItem span { - min-width: 0; - overflow: hidden; - color: var(--monitor-muted); - font-size: 12px; - font-weight: 800; - line-height: 1.2; - text-overflow: ellipsis; - white-space: nowrap; -} - -.trendSummaryItem strong { - min-width: 0; - overflow: hidden; - color: var(--monitor-ink); - font-size: 18px; - font-weight: 850; - letter-spacing: -0.03em; - line-height: 1.1; - text-overflow: ellipsis; - white-space: nowrap; -} - -.usageTrendSvg { - display: block; - width: 100%; - height: 260px; - min-height: 0; - overflow: visible; -} - -.chartGridLine { - stroke: color-mix(in srgb, var(--monitor-line) 82%, transparent); - stroke-dasharray: 4 7; - stroke-width: 1; -} - -.chartAxisBase, -.chartYAxisRequests, -.chartYAxisCost, -.chartYAxisTokens, -.chartXAxisTick { - shape-rendering: crispEdges; -} - -.chartAxisBase { - stroke: color-mix(in srgb, var(--monitor-ink) 52%, transparent); - stroke-width: 1.1; -} - -.chartYAxisRequests, -.chartXAxisTick { - stroke: #2563eb; - stroke-width: 1.1; -} - -.chartYAxisCost { - stroke: #047857; - stroke-width: 1.1; -} - -.chartYAxisTokens { - stroke: #7c3aed; - stroke-width: 1.1; -} - -.chartAxisLabel, -.chartXAxisLabel { - font-size: 13px; - font-weight: 650; - dominant-baseline: middle; -} - -.chartAxisLabel { - text-anchor: end; -} - -.chartAxisLabelRequests { - fill: #2563eb; -} - -.chartAxisLabelCost { - fill: #047857; - text-anchor: end; -} - -.chartAxisLabelTokens { - fill: #7c3aed; - text-anchor: end; -} - -.chartXAxisLabel { - fill: color-mix(in srgb, var(--monitor-ink) 66%, var(--monitor-muted)); - text-anchor: middle; -} - -.trendAreaFill { - opacity: 0.92; -} - -.trendSeriesLine { - fill: none; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 2.8; -} - -.trendSeriesDot { - fill: var(--monitor-surface); - stroke-width: 2.4; - transition: r 0.15s ease; -} - -.trendHoverTarget { - outline: none; -} - -.trendHoverGuide { - stroke: color-mix(in srgb, var(--monitor-muted) 35%, transparent); - stroke-width: 1; -} - -.trendTooltipLayer rect { - fill: color-mix(in srgb, var(--monitor-surface) 98%, var(--monitor-bg)); - stroke: color-mix(in srgb, var(--monitor-line-strong) 68%, transparent); - stroke-width: 1; - filter: drop-shadow(0 14px 26px rgba(15, 23, 42, 0.18)); -} - -.trendTooltipLayer text { - font-size: 13px; - font-weight: 700; -} - -.trendTooltipTitle { - fill: var(--monitor-ink); - font-size: 14px !important; - font-weight: 800 !important; -} - -.trendTooltipMetric { - font-size: 13px !important; -} - -.tokenStatCardList { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - align-content: stretch; - grid-auto-rows: minmax(78px, 1fr); - gap: 10px; - min-width: 0; - min-height: 0; - overflow: visible; -} - -.tokenStatCard { - --token-stat-accent: #6366f1; - --token-stat-border: color-mix(in srgb, var(--token-stat-accent) 32%, var(--monitor-line)); - --token-stat-bg: color-mix(in srgb, var(--token-stat-accent) 8%, var(--monitor-surface)); - - display: grid; - align-content: space-between; - gap: 7px; - min-width: 0; - min-height: 0; - padding: 12px 14px; - border: 1px solid var(--token-stat-border); - border-radius: 12px; - background: var(--token-stat-bg); -} - -.tokenStatCardPurple { - --token-stat-accent: #6d5dfc; -} - -.tokenStatCardBlue { - --token-stat-accent: #3b82f6; -} - -.tokenStatCardCyan { - --token-stat-accent: #06b6d4; -} - -.tokenStatCardGreen { - --token-stat-accent: #22c55e; -} - -.tokenStatCardAmber { - --token-stat-accent: #f59e0b; -} - -.tokenStatCardRose { - --token-stat-accent: #fb7185; -} - -.tokenStatCardIndigo { - --token-stat-accent: #8b5cf6; -} - -.tokenStatCardSlate { - --token-stat-accent: #64748b; -} - -.tokenStatCardHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - min-width: 0; -} - -.tokenStatCardHeader span, -.tokenStatCardHeader strong { - color: color-mix(in srgb, var(--monitor-ink) 78%, var(--monitor-muted)); - font-size: 13px; - font-weight: 800; - letter-spacing: -0.01em; -} - -.tokenStatCardHeader strong { - flex: 0 0 auto; - color: var(--monitor-muted); - font-size: 12px; - letter-spacing: 0.01em; -} - -.tokenStatCardValue { - color: var(--monitor-ink); - font-size: clamp(21px, 2.5vw, 28px); - font-weight: 800; - line-height: 1; - letter-spacing: -0.04em; -} - -.tokenStatProgressTrack { - height: 4px; - overflow: hidden; - border-radius: 999px; - background: color-mix(in srgb, var(--monitor-line) 58%, transparent); -} - -.tokenStatProgressTrack span { - display: block; - width: var(--token-stat-width, 0%); - height: 100%; - border-radius: inherit; - background: var(--token-stat-accent); -} - -.rankingGrid { - display: grid; - grid-template-columns: minmax(0, 1.22fr) minmax(360px, 0.78fr); - gap: 16px; - align-items: stretch; - --ranking-card-height: 500px; -} - -.modelStatsCard, -.apiKeyRankingCard { - min-width: 0; - padding: 20px; - border: 1px solid var(--monitor-line); - border-radius: 12px; - background: var(--monitor-surface); - box-shadow: var(--shadow); -} - -.modelStatsCard, -.apiKeyRankingCard { - display: grid; - grid-template-rows: auto minmax(0, 1fr); - height: var(--ranking-card-height); -} - -.modelStatsCard { - overflow: visible; -} - -.apiKeyRankingCard { - overflow: hidden; -} - -.modelStatsCard { - position: relative; - z-index: 2; - --donut-color-0: #2563eb; - --donut-color-1: #22c55e; - --donut-color-2: #f97316; - --donut-color-3: #8b5cf6; - --donut-color-4: #06b6d4; -} - -.rankingHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - margin-bottom: 18px; - min-width: 0; -} - -.rankingHeader h3 { - margin: 0; - font-size: 18px; - font-weight: 700; - letter-spacing: -0.02em; -} - -.rankingHeader > .rankingMetricSwitch { - flex-shrink: 0; - align-self: flex-start; -} - -.rankingMetricSwitch { - display: inline-flex; - flex: 0 0 auto; - gap: 3px; - padding: 3px; - border-radius: 999px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 80%, transparent); - background: color-mix(in srgb, var(--monitor-surface-muted) 78%, var(--monitor-surface)); -} - -.rankingMetricButton { - min-height: 28px; - padding: 0 10px; - border: 0; - border-radius: 999px; - background: transparent; - color: var(--monitor-muted); - font-size: 12px; - font-weight: 700; - cursor: pointer; - transition: - background-color 0.18s ease, - color 0.18s ease, - box-shadow 0.18s ease; -} - -.rankingMetricButton:hover:not(:disabled) { - color: var(--monitor-ink); -} - -.rankingMetricButton:disabled { - cursor: not-allowed; - opacity: 0.45; -} - -.rankingMetricButtonActive { - background: var(--monitor-accent); - color: var(--primary-contrast, #fff); - box-shadow: 0 8px 18px -14px rgba(0, 0, 0, 0.45); -} - -.rankingHeader p { - margin: 6px 0 0; - color: var(--monitor-muted); - font-size: 13px; - line-height: 1.5; -} - -.modelStatsLayout { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(220px, 260px); - gap: 16px; - align-items: stretch; - min-height: 0; - overflow: visible; -} - -.modelStatsLayout > * { - min-height: 0; - max-height: 100%; -} - -.modelStatsList, -.apiKeyRankingScroll { - display: grid; - align-content: start; - grid-auto-rows: max-content; - gap: 10px; -} - -.modelStatsList, -.apiKeyRankingScroll { - min-height: 0; - max-height: 100%; - overflow: auto; - overscroll-behavior: contain; - padding-right: 4px; -} - -.apiKeyRankingList { - display: grid; - grid-template-rows: auto minmax(0, 1fr); - gap: 12px; - min-height: 0; -} - -.modelStatsRow, -.apiKeyRankingRow { - min-width: 0; - padding: 12px; - border-radius: 12px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); - background: color-mix(in srgb, var(--monitor-surface-elevated) 82%, var(--monitor-surface)); -} - -.modelStatsRow { - display: grid; - grid-template-columns: 1fr; - align-items: center; - gap: 12px; -} - -.modelStatsRow, -.apiKeyRankingRow { - transition: - transform 0.18s ease, - border-color 0.18s ease, - background-color 0.18s ease; -} - -.modelStatsRow:hover, -.apiKeyRankingRow:hover { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--monitor-accent) 28%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-accent) 5%, var(--monitor-surface-elevated)); -} - -.rankingIndex { - display: inline-flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - flex: 0 0 auto; - border-radius: 999px; - background: var(--monitor-accent-soft); - color: var(--monitor-accent-strong); - font-size: 12px; - font-weight: 800; -} - -.modelStatsMain, -.apiKeyRankingRow { - display: grid; - gap: 9px; - min-width: 0; -} - -.modelStatsTitleLine, -.apiKeyRankingTopLine, -.apiKeyRankingName, -.modelStatsMetaLine, -.apiKeyRankingMetaLine { - display: flex; - align-items: center; - min-width: 0; -} - -.modelStatsTitleLine, -.apiKeyRankingTopLine { - justify-content: space-between; - gap: 12px; -} - -.modelStatsTitleLine strong, -.apiKeyRankingName strong { - min-width: 0; - overflow: hidden; - color: var(--monitor-ink); - font-size: 14px; - font-weight: 700; - text-overflow: ellipsis; - white-space: nowrap; -} - -.modelStatsTitleLine span, -.apiKeyRankingTopLine > span { - flex: 0 0 auto; - color: var(--monitor-ink); - font-size: 13px; - font-weight: 800; -} - -.modelStatsTitleLine span { - padding: 3px 8px; - border-radius: 999px; - background: var(--monitor-accent-soft); - color: var(--monitor-accent-strong); -} - -.modelStatsMetaLine, -.apiKeyRankingMetaLine { - flex-wrap: wrap; - gap: 8px 12px; - color: var(--monitor-muted); - font-size: 12px; -} - -.modelStatsBar, -.apiKeyRankingBar { - position: relative; - overflow: hidden; - height: 8px; - border-radius: 999px; - background: color-mix(in srgb, var(--monitor-line) 70%, transparent); -} - -.modelStatsBar::before, -.apiKeyRankingBar::before { - content: ''; - position: absolute; - inset: 0; - border-radius: inherit; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.28), transparent); - opacity: 0.35; -} - -.modelStatsBar::after, -.apiKeyRankingBar::after { - content: ''; - position: absolute; - inset: 0 auto 0 0; - width: var(--ranking-width, 0%); - border-radius: inherit; -} - -.modelStatsBar::after { - background: linear-gradient(90deg, var(--monitor-accent), var(--monitor-cyan)); -} - -.apiKeyRankingBar::after { - background: linear-gradient(90deg, #22c55e, #86efac); -} - -.modelSharePanel { - display: grid; - grid-template-rows: auto minmax(0, auto) auto; - align-content: center; - justify-items: center; - gap: 12px; - min-width: 0; - min-height: 0; - max-height: 100%; - padding: 12px; - border-radius: 16px; - border: 1px solid color-mix(in srgb, var(--monitor-accent) 16%, var(--monitor-line)); - background: - radial-gradient(circle at 50% 20%, color-mix(in srgb, var(--monitor-accent) 10%, transparent), transparent 38%), - color-mix(in srgb, var(--monitor-surface-muted) 72%, var(--monitor-surface)); -} - -.modelShareHeader { - display: grid; - gap: 4px; - justify-self: stretch; - text-align: left; -} - -.modelShareHeader strong { - font-size: 15px; - line-height: 1.25; -} - -.modelShareHeader span { - color: var(--monitor-muted); - font-size: 12px; -} - -.donutChart { - position: relative; - display: grid; - place-items: center; - width: min(170px, 100%); - aspect-ratio: 1; - border-radius: 50%; - background: var(--donut-bg); -} - -.donutChart::after { - content: ''; - position: absolute; - inset: 22%; - border-radius: 50%; - background: var(--monitor-surface); - box-shadow: inset 0 0 0 1px var(--monitor-line); -} - -.donutCenter { - position: relative; - z-index: 1; - display: grid; - gap: 5px; - justify-items: center; - max-width: 78%; - text-align: center; -} - -.donutCenter span { - color: var(--monitor-muted); - font-size: 12px; -} - -.donutCenter strong { - max-width: 100%; - overflow: hidden; - font-size: 18px; - font-weight: 800; - text-overflow: ellipsis; - white-space: nowrap; -} - -.donutTooltip { - position: absolute; - right: 50%; - bottom: calc(100% + 12px); - z-index: 5; - width: min(300px, calc(100vw - 48px)); - max-height: 260px; - overflow: auto; - padding: 12px; - border-radius: 12px; - border: 1px solid var(--monitor-line); - background: color-mix(in srgb, var(--monitor-surface-elevated) 96%, var(--monitor-bg)); - box-shadow: 0 18px 42px -26px rgba(15, 23, 42, 0.55); - opacity: 0; - pointer-events: none; - transform: translate(50%, 6px); - transition: - opacity 0.16s ease, - transform 0.16s ease; -} - -.donutChart:hover .donutTooltip, -.donutChart:focus-within .donutTooltip { - opacity: 1; - transform: translate(50%, 0); -} - -.donutTooltip strong { - display: block; - margin-bottom: 8px; - color: var(--monitor-ink); - font-size: 12px; -} - -.donutTooltip div { - display: grid; - gap: 7px; -} - -.donutTooltip span { - display: grid; - grid-template-columns: 8px minmax(0, 1fr) auto; - align-items: center; - gap: 8px; - min-width: 0; - color: var(--monitor-muted); - font-size: 12px; -} - -.donutTooltip i { - width: 8px; - height: 8px; - border-radius: 999px; -} - -.donutTooltip em { - min-width: 0; - overflow: hidden; - font-style: normal; - text-overflow: ellipsis; - white-space: nowrap; -} - -.donutTooltip b { - color: var(--monitor-ink); - font-size: 12px; -} - -.modelSharePanel .donutTooltip { - right: auto; - bottom: auto; - left: calc(100% + 14px); - top: 50%; - transform: translate(0, -50%); -} - -.modelSharePanel .donutChart:hover .donutTooltip, -.modelSharePanel .donutChart:focus-within .donutTooltip { - transform: translate(0, -50%); -} - -.modelSharePanel .modelLegend { - margin-top: 2px; -} - -.modelLegend { - display: grid; - gap: 6px; - width: 100%; - min-width: 0; - min-height: 0; - overflow: visible; -} - -.modelLegendItem { - display: grid; - grid-template-columns: 10px minmax(0, 1fr) auto; - align-items: center; - gap: 8px; - min-width: 0; - color: var(--monitor-muted); - font-size: 12px; -} - -.modelLegendItem span:nth-child(2) { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.modelLegendItem strong { - color: var(--monitor-ink); - font-size: 12px; -} - -.modelLegendDot { - width: 8px; - height: 8px; - border-radius: 999px; - background: var(--legend-color, var(--donut-color-0)); -} - -.apiKeyRankingCard { - display: grid; - align-self: start; - align-content: start; -} - -.apiKeyRankingName { - gap: 10px; -} - -.apiKeyRankingSummary { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 4px 12px; - align-items: end; - padding: 16px; - border-radius: 14px; - border: 1px solid color-mix(in srgb, #22c55e 24%, var(--monitor-line)); - background: - radial-gradient(circle at 100% 0%, rgba(34, 197, 94, 0.14), transparent 42%), - color-mix(in srgb, #22c55e 7%, var(--monitor-surface)); -} - -.apiKeyRankingSummary span, -.apiKeyRankingSummary small { - color: var(--monitor-muted); - font-size: 12px; -} - -.apiKeyRankingSummary strong { - grid-row: 1 / span 2; - grid-column: 2; - color: var(--monitor-ink); - font-size: 24px; - font-weight: 800; - letter-spacing: -0.03em; - line-height: 1; -} - -.apiKeyRankingSummary small { - grid-column: 1; -} - -.apiKeyRankingList { - gap: 14px; -} - -.accountStatsCard { - display: grid; - gap: 14px; - min-width: 0; - padding: 18px; - border: 1px solid var(--monitor-line); - border-radius: 12px; - background: var(--monitor-surface); - box-shadow: var(--shadow); - overflow: visible; -} - -.accountStatsToolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - flex-wrap: wrap; -} - -.accountStatsFilters { - display: flex; - align-items: center; - gap: 8px; - flex: 1 1 auto; - min-width: 0; - flex-wrap: wrap; -} - -.accountStatsFilters :global(.form-group) { - margin: 0; -} - -.accountStatsSearchInput { - width: 180px; - min-width: 0; - height: 34px; - min-height: 34px; - padding-block: 0; - padding-right: 30px; - border-radius: 8px; - font-size: 12px; - line-height: 1; -} - -.accountStatsSelect { - height: 34px; - min-height: 34px; - padding: 0 8px; - border: 1px solid var(--monitor-line); - border-radius: 8px; - background: var(--monitor-surface); - color: var(--monitor-ink); - font-size: 12px; - font-weight: 600; - cursor: pointer; - appearance: auto; -} - -.accountStatsSelect:hover { - border-color: color-mix(in srgb, var(--monitor-accent) 35%, var(--monitor-line-strong)); -} - -.accountStatsClearButton { - display: inline-flex; - align-items: center; - justify-content: center; - height: 34px; - min-height: 34px; - padding: 0 10px; - border: 1px solid var(--monitor-line); - border-radius: 8px; - background: var(--monitor-surface); - color: var(--monitor-muted); - font-size: 12px; - font-weight: 600; - cursor: pointer; - white-space: nowrap; - transition: - border-color 0.18s ease, - background-color 0.18s ease, - color 0.18s ease; -} - -.accountStatsClearButton:hover { - border-color: color-mix(in srgb, var(--monitor-accent) 35%, var(--monitor-line-strong)); - background: var(--monitor-surface-hover); - color: var(--monitor-ink); -} - -.accountOverviewCardGrid { - --account-card-min-width: 330px; - - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--account-card-min-width)), 1fr)); - align-items: start; - justify-content: start; - gap: 16px; -} - -.accountOverviewCard { - display: grid; - gap: 14px; - align-self: start; - width: 100%; - height: auto; - min-width: 0; - padding: 16px; - border: 1px solid color-mix(in srgb, var(--monitor-line-strong) 58%, var(--monitor-line)); - border-radius: 16px; - background: color-mix(in srgb, var(--floating-surface, #fffdf9) 34%, var(--monitor-surface)); - box-shadow: - inset 0 1px 0 color-mix(in srgb, var(--floating-surface, #fffdf9) 48%, transparent), - 0 8px 18px -18px rgba(45, 42, 38, 0.34); -} - -.accountOverviewCardExpanded { - border-color: color-mix(in srgb, var(--monitor-accent) 28%, var(--monitor-line-strong)); - box-shadow: - inset 0 1px 0 color-mix(in srgb, var(--floating-surface, #fffdf9) 52%, transparent), - 0 10px 20px -18px rgba(45, 42, 38, 0.38), - 0 0 0 1px color-mix(in srgb, var(--monitor-accent) 8%, transparent); -} - -.accountOverviewCardFocused { - border-color: color-mix(in srgb, var(--monitor-accent) 42%, var(--monitor-line)); - box-shadow: - 0 0 0 1px color-mix(in srgb, var(--monitor-accent) 18%, transparent), - 0 12px 24px -18px rgba(45, 42, 38, 0.42), - var(--shadow-lg); -} - -.accountOverviewCardHeader { - display: grid; - gap: 8px; - min-width: 0; -} - -.accountTitleRow { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: start; - gap: 12px; - min-width: 0; -} - -.accountExpandGlyph { - display: inline-flex; - width: 18px; - height: 18px; - align-items: center; - justify-content: center; - color: var(--monitor-muted); - align-self: center; -} - -.accountIdentityLine { - display: inline-flex; - align-items: center; - gap: 8px; - min-width: 0; -} - -.accountStatusDot { - width: 8px; - height: 8px; - border-radius: 999px; - background: currentColor; - flex: 0 0 auto; -} - -.accountStatusDotEnabled { - color: var(--monitor-green); -} - -.accountStatusDotDisabled, -.accountStatusDotUnavailable { - color: var(--monitor-red); -} - -.accountStatusDotMixed { - color: var(--monitor-amber); -} - -.accountHealthBadge { - display: inline-flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - min-height: 26px; - padding: 0 10px; - border: 1px solid transparent; - border-radius: 999px; - font-size: 12px; - font-weight: 800; - white-space: nowrap; -} - -.accountHealthBadgegood { - color: var(--monitor-green); - border-color: color-mix(in srgb, var(--monitor-green) 26%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-green) 11%, var(--monitor-surface)); -} - -.accountHealthBadgewarn { - color: color-mix(in srgb, var(--monitor-amber) 86%, #78350f); - border-color: color-mix(in srgb, var(--monitor-amber) 30%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-amber) 15%, var(--monitor-surface)); -} - -.accountHealthBadgebad { - color: var(--monitor-red); - border-color: color-mix(in srgb, var(--monitor-red) 26%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-red) 11%, var(--monitor-surface)); -} - -.accountOverviewCardTimestamp { - min-width: 0; - overflow: hidden; - color: var(--monitor-muted); - font-size: 12px; - line-height: 1.45; - text-overflow: ellipsis; - white-space: nowrap; -} - -.accountMetaRow { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - min-width: 0; -} - -.accountMetaRow .inlineActionButton { - margin-left: auto; -} - -.accountFocusButton { - min-height: 26px; - padding: 0 8px; - gap: 5px; - border-color: color-mix(in srgb, var(--monitor-line) 56%, transparent); - background: color-mix(in srgb, var(--monitor-surface-elevated) 58%, transparent); - color: var(--monitor-muted); - font-size: 11px; - font-weight: 700; -} - -.accountMetaSeparator { - color: var(--monitor-muted); -} - -.accountOverviewStatusSection, -.accountTokenStructurePanel { - display: grid; - gap: 12px; - min-width: 0; - padding: 16px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); - border-radius: 12px; - background: color-mix(in srgb, var(--monitor-surface-muted) 34%, var(--monitor-surface)); -} - -.accountScopeText { - color: var(--monitor-muted); - font-size: 11px; - line-height: 1.35; -} - -.accountTokenPanel { - display: grid; - gap: 12px; - min-width: 0; -} - -.accountSectionInfo { - display: inline-flex; - width: 16px; - height: 16px; - align-items: center; - justify-content: center; - border-radius: 999px; - border: 1px solid color-mix(in srgb, var(--monitor-muted) 30%, transparent); - color: var(--monitor-muted); - font-size: 10px; - font-weight: 800; -} - -.healthMetricGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; -} - -.healthMetricItem { - display: grid; - gap: 5px; - min-width: 0; -} - -.healthMetricItem span { - min-width: 0; - overflow: hidden; - color: var(--monitor-muted); - font-size: 11px; - line-height: 1.25; - text-overflow: ellipsis; - white-space: nowrap; -} - -.healthMetricItem strong { - min-width: 0; - overflow: hidden; - color: var(--monitor-ink); - font-size: 16px; - font-weight: 750; - line-height: 1.25; - text-overflow: ellipsis; - white-space: nowrap; -} - -.healthMetricItem .successFailureValue, -.healthMetricItem .successFailureValue span { - font-size: inherit; - font-weight: inherit; - line-height: inherit; -} - -.monitoringStatusBar { - display: flex; - align-items: center; - gap: 8px; -} - -.monitoringStatusBlocks { - display: flex; - gap: 3px; - flex: 1; - min-width: 0; - position: relative; -} - -.monitoringStatusBlockWrapper { - flex: 1; - min-width: 6px; - position: relative; -} - -.monitoringStatusBlockButton { - width: 100%; - padding: 3px 0; - display: flex; - align-items: center; - background: transparent; - border: 0; - border-radius: 999px; - cursor: pointer; - -webkit-tap-highlight-color: transparent; -} - -.monitoringStatusBlockButton:focus-visible { - outline: 2px solid color-mix(in srgb, var(--monitor-accent) 78%, white); - outline-offset: 2px; -} - -.monitoringStatusBlockActive .monitoringStatusBlock, -.monitoringStatusBlockButton:hover .monitoringStatusBlock, -.monitoringStatusBlockButton:focus-visible .monitoringStatusBlock { - transform: scaleY(1.35); - opacity: 1; -} - -.monitoringStatusBlock { - width: 100%; - height: 7px; - border-radius: 999px; - opacity: 0.92; - transform-origin: center; - transition: - transform 0.15s ease, - opacity 0.15s ease; -} - -.monitoringStatusBlockIdle { - background-color: color-mix(in srgb, var(--monitor-line) 82%, transparent); -} - -.monitoringStatusTooltip { - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - z-index: 20; - padding: 6px 10px; - border: 1px solid var(--monitor-line); - border-radius: 8px; - background: var(--monitor-surface); - box-shadow: var(--shadow-lg); - color: var(--monitor-ink); - font-size: 11px; - line-height: 1.5; - white-space: nowrap; - pointer-events: none; -} - -.monitoringStatusTooltip::after { - content: ''; - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - border: 5px solid transparent; - border-top-color: var(--monitor-surface); -} - -.monitoringStatusTooltip::before { - content: ''; - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - border: 6px solid transparent; - border-top-color: var(--monitor-line); -} - -.monitoringStatusTooltipLeft { - left: 0; - transform: translateX(0); -} - -.monitoringStatusTooltipLeft::after, -.monitoringStatusTooltipLeft::before { - left: 8px; - transform: none; -} - -.monitoringStatusTooltipRight { - left: auto; - right: 0; - transform: translateX(0); -} - -.monitoringStatusTooltipRight::after, -.monitoringStatusTooltipRight::before { - left: auto; - right: 8px; - transform: none; -} - -.monitoringTooltipTime { - display: block; - margin-bottom: 2px; - color: var(--monitor-muted); -} - -.monitoringTooltipStats { - display: flex; - align-items: center; - gap: 8px; -} - -.monitoringTooltipSuccess { - color: var(--monitor-green); -} - -.monitoringTooltipFailure { - color: var(--monitor-red); -} - -.monitoringTooltipRate { - color: var(--monitor-muted); -} - -.monitoringStatusRate { - display: inline-flex; - align-items: center; - min-height: 30px; - padding: 0 10px; - border-radius: 999px; - background: color-mix(in srgb, var(--monitor-surface-elevated) 88%, var(--monitor-surface)); - color: var(--monitor-muted); - font-size: 12px; - font-weight: 700; - white-space: nowrap; -} - -.monitoringStatusRatePlaceholder { - display: none; -} - -.monitoringStatusRateHigh { - color: var(--monitor-green); - background: color-mix(in srgb, var(--monitor-green) 14%, var(--monitor-surface)); -} - -.monitoringStatusRateMedium { - color: color-mix(in srgb, var(--monitor-amber) 86%, #78350f); - background: color-mix(in srgb, var(--monitor-amber) 16%, var(--monitor-surface)); -} - -.monitoringStatusRateLow { - color: var(--monitor-red); - background: color-mix(in srgb, var(--monitor-red) 14%, var(--monitor-surface)); -} - -.accountOverviewMetricGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; - align-items: start; -} - -.accountOverviewMetricCard { - display: flex; - flex-direction: column; - justify-content: center; - gap: 5px; - min-height: 68px; - min-width: 0; - padding: 12px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 80%, transparent); - border-radius: 10px; - background: color-mix(in srgb, var(--monitor-surface-elevated) 80%, var(--monitor-surface)); -} - -.accountOverviewMetricLabel { - display: inline-flex; - align-items: center; - gap: 4px; - min-width: 0; - color: var(--monitor-muted); - font-size: 10px; - font-weight: 700; - line-height: 1.3; - white-space: nowrap; -} - -.accountMetricIcon { - display: inline-flex; - width: 14px; - height: 14px; - align-items: center; - justify-content: center; - flex: 0 0 auto; - border-radius: 6px; - background: color-mix(in srgb, var(--monitor-accent) 11%, var(--monitor-surface)); - color: var(--monitor-accent-strong); -} - -.accountMetricIcon::before { - content: ''; - width: 6px; - height: 6px; - border-radius: 999px; - background: currentColor; -} - -.accountMetricIconTotal { - background: color-mix(in srgb, var(--monitor-accent) 13%, var(--monitor-surface)); - color: var(--monitor-accent-strong); -} - -.accountMetricIconInput { - background: color-mix(in srgb, var(--monitor-green) 13%, var(--monitor-surface)); - color: var(--monitor-green); -} - -.accountMetricIconOutput { - background: color-mix(in srgb, var(--monitor-amber) 17%, var(--monitor-surface)); - color: var(--monitor-amber); -} - -.accountMetricIconCached { - background: color-mix(in srgb, var(--monitor-cyan) 14%, var(--monitor-surface)); - color: var(--monitor-cyan); -} - -.accountOverviewMetricValue { - min-width: 0; - font-size: 16px; - font-weight: 750; - line-height: 1.35; - white-space: nowrap; - overflow-wrap: normal; - word-break: normal; -} - -.accountOverviewCardBody { - display: grid; - gap: 14px; - padding-top: 2px; - min-width: 0; -} - -.accountModelListPanel { - display: grid; - align-content: start; - gap: 10px; - min-width: 0; -} - -.accountModelViewAllButton { - display: inline-flex; - align-items: center; - min-height: 28px; - padding: 0; - border: 0; - background: transparent; - color: var(--monitor-accent-strong); - font: inherit; - font-size: 12px; - font-weight: 700; - cursor: pointer; -} - -.accountModelViewAllButton:hover { - transform: none; - color: var(--monitor-accent); -} - -.accountModelList { - display: grid; - min-width: 0; -} - -.accountModelItem { - display: grid; - gap: 6px; - min-width: 0; - padding: 10px 0; - border-top: 1px solid color-mix(in srgb, var(--monitor-line) 84%, transparent); -} - -.accountModelItem:first-child { - border-top: 0; -} - -.accountModelRow { - display: grid; - align-items: start; - gap: 6px; - width: 100%; - min-width: 0; - padding: 0; - border: 0; - background: transparent; - color: inherit; - font: inherit; - text-align: left; - cursor: pointer; -} - -.accountModelRow:hover { - transform: none; - color: var(--monitor-accent-strong); -} - -.accountModelStat { - display: inline-flex; - align-items: baseline; - gap: 3px; - min-width: 0; - text-align: left; -} - -.accountModelStat small { - color: var(--monitor-muted); - font-size: 11px; - font-weight: 700; - line-height: 1.4; - letter-spacing: 0; - white-space: nowrap; -} - -.accountModelStat strong { - min-width: 0; - overflow: hidden; - color: var(--monitor-ink); - font-size: 11px; - font-weight: 700; - line-height: 1.4; - text-overflow: ellipsis; - white-space: nowrap; -} - -.accountModelMetaLine { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 4px 8px; - min-width: 0; - color: var(--monitor-muted); -} - -.accountModelMetaLine .accountModelStat { - min-width: 0; -} - -.accountModelChevron { - display: inline-flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - width: 14px; - min-width: 14px; - height: 16px; - color: var(--monitor-muted); - line-height: 1; -} - -.accountModelExpanded { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 8px 12px; - padding: 8px 10px 4px; - border-top: 1px dashed color-mix(in srgb, var(--monitor-line) 80%, transparent); -} - -.accountModelExpandedItem { - display: grid; - gap: 2px; - min-width: 0; -} - -.accountModelExpandedItem small { - min-width: 0; - overflow: hidden; - color: var(--monitor-muted); - font-size: 10px; - font-weight: 700; - line-height: 1.2; - letter-spacing: 0; - text-transform: uppercase; - text-overflow: ellipsis; - white-space: nowrap; -} - -.accountModelExpandedItem strong { - min-width: 0; - overflow: hidden; - color: var(--monitor-ink); - font-size: 12px; - font-weight: 700; - line-height: 1.3; - text-overflow: ellipsis; - white-space: nowrap; -} - -.accountModelName { - display: inline-block; - min-width: 0; - max-width: 100%; - overflow: hidden; - color: var(--monitor-ink); - font-weight: 700; - text-overflow: ellipsis; - white-space: nowrap; -} - -.summaryCard { - display: grid; - gap: 6px; - min-width: 0; - min-height: 104px; - padding: 16px; - border-radius: 8px; - background: var(--monitor-surface); - transition: - transform 0.18s ease, - border-color 0.18s ease, - box-shadow 0.18s ease; -} - -.summaryCard:hover { - transform: translateY(-1px); - border-color: var(--monitor-line-strong); - box-shadow: var(--shadow); -} - -.summaryLabel { - color: var(--monitor-muted); - font-size: 13px; - font-weight: 500; - letter-spacing: 0; - text-transform: none; -} - -.summaryValue { - min-width: 0; - max-width: 100%; - overflow: hidden; - font-size: 22px; - font-weight: 800; - line-height: 1.1; - letter-spacing: 0; - text-overflow: ellipsis; - white-space: nowrap; -} - -.summaryMeta { - margin-top: 0; - color: var(--monitor-muted); - font-size: 12px; - line-height: 1.5; -} - -.statusBadge { - gap: 6px; - min-height: 24px; - padding: 0 8px; - justify-content: center; - font-size: 12px; - font-weight: 600; - white-space: nowrap; -} - -.tonegood { - color: var(--monitor-green); -} - -.tonewarn { - color: var(--monitor-amber); -} - -.tonebad { - color: var(--monitor-red); -} - -.statusBadge.tonegood { - border-color: color-mix(in srgb, var(--monitor-green) 26%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-green) 10%, var(--monitor-surface)); -} - -.statusBadge.tonewarn { - border-color: color-mix(in srgb, var(--monitor-amber) 24%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-amber) 10%, var(--monitor-surface)); -} - -.statusBadge.tonebad { - border-color: color-mix(in srgb, var(--monitor-red) 26%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-red) 10%, var(--monitor-surface)); -} - -.successFailureValue { - display: inline-flex; - align-items: baseline; - gap: 0.12em; - min-width: 0; -} - -.goodText { - color: var(--monitor-green); -} - -.warnText { - color: var(--monitor-amber); -} - -.badText { - color: var(--monitor-red); -} - -.patternBars { - display: inline-grid; - grid-auto-flow: column; - grid-auto-columns: 6px; - gap: 4px; - align-items: center; - padding: 7px 10px; - border-radius: 999px; - border: 1px solid var(--monitor-line); - background: var(--monitor-surface-muted); -} - -.patternBarsPlain { - grid-auto-columns: 4px; - gap: 2px; - padding: 0; - border: 0; - border-radius: 0; - background: transparent; -} - -.patternBar { - width: 6px; - height: 16px; - border-radius: 999px; - background: color-mix(in srgb, var(--monitor-muted) 24%, transparent); -} - -.patternBarPlain { - width: 4px; - height: 24px; -} - -.patternSuccess { - background: var(--monitor-green); -} - -.patternFailed { - background: var(--monitor-red); -} - -.monitoringSettingsEditor { - margin-top: 14px; - display: grid; - gap: 14px; -} - -.settingsSectionCard { - display: grid; - gap: 14px; - padding: 16px; - border: 1px solid color-mix(in srgb, var(--monitor-line-strong) 30%, var(--monitor-line)); - border-radius: 16px; - background: var(--monitor-surface-elevated); -} - -.settingsDangerSection { - border-color: color-mix(in srgb, var(--monitor-red) 32%, var(--monitor-line)); -} - -.settingsDangerAction { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding-top: 12px; - border-top: 1px solid color-mix(in srgb, var(--monitor-red) 18%, var(--monitor-line)); -} - -.settingsDangerAction > div { - display: grid; - gap: 3px; - min-width: 0; -} - -.settingsDangerAction > div > span { - color: var(--monitor-muted); - font-size: 12px; -} - -.settingsDangerAction strong { - color: var(--monitor-ink); - font-size: 16px; -} - -.settingsDangerAction :global(button) { - flex: 0 0 auto; -} - -.resetStatisticsButton:global(.btn-danger) { - min-height: 40px; - padding-inline: 16px; - border-color: var(--monitor-red); - background: var(--monitor-red); - color: #fff; - box-shadow: 0 8px 18px -12px color-mix(in srgb, var(--monitor-red) 78%, transparent); -} - -.resetStatisticsButton:global(.btn-danger) > span { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 7px; - color: inherit; - line-height: 1.2; -} - -.resetStatisticsButton:global(.btn-danger) svg { - flex: 0 0 auto; - color: inherit; - stroke: currentColor; -} - -.resetStatisticsButton:global(.btn-danger):hover:not(:disabled) { - border-color: color-mix(in srgb, var(--monitor-red) 86%, #000); - background: color-mix(in srgb, var(--monitor-red) 86%, #000); - color: #fff; - box-shadow: 0 10px 22px -12px color-mix(in srgb, var(--monitor-red) 88%, transparent); - transform: translateY(-1px); -} - -.resetStatisticsButton:global(.btn-danger):focus-visible { - outline: 3px solid color-mix(in srgb, var(--monitor-red) 28%, transparent); - outline-offset: 2px; -} - -.resetStatisticsButton:global(.btn-danger):disabled { - color: #fff; - opacity: 0.68; - transform: none; -} - -.settingsSectionHeader { - display: grid; - gap: 4px; -} - -.settingsSectionHeader strong { - color: var(--monitor-ink); - font-size: 14px; - font-weight: 800; -} - -.settingsSectionHeader span, -.settingsField small, -.settingsHint, -.settingsScheduleNote { - color: var(--monitor-muted); - font-size: 12px; - line-height: 1.5; -} - -.settingsField { - display: grid; - align-content: start; - gap: 8px; - min-width: 0; - color: var(--monitor-ink); - font-size: 13px; - font-weight: 700; -} - -.settingsField :global(.form-group) { - margin-bottom: 0; -} - -.settingsField input { - min-height: 42px; -} - -.settingsScheduleNote { - padding: 10px 12px; - border: 1px solid color-mix(in srgb, var(--monitor-accent) 18%, var(--monitor-line)); - border-radius: 12px; - background: color-mix(in srgb, var(--monitor-accent) 7%, var(--monitor-surface)); -} - -.settingsGrid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - align-items: start; - gap: 12px; -} - -.settingsCheckboxField { - display: inline-flex; - align-items: center; - gap: 8px; - color: var(--monitor-ink); - font-size: 13px; - font-weight: 700; -} - -.settingsCheckboxField input { - width: 16px; - height: 16px; - accent-color: var(--monitor-accent); -} - -.priceField { - display: grid; - gap: 7px; - min-width: 0; - color: var(--monitor-muted); - font-size: 12px; - font-weight: 700; -} - -.priceField :global(.form-group) { - margin-bottom: 0; -} - -.priceField input { - min-height: 40px; -} - -.priceActionsBar { - margin-top: 2px; - display: flex; - justify-content: flex-end; - gap: 10px; -} - -.priceManagerModal :global(.modal-body) { - max-height: min(72vh, 640px); - overflow: hidden; - padding: 0; -} - -.priceManager { - display: grid; - grid-template-rows: auto minmax(0, 1fr); - background: var(--monitor-surface); -} - -.priceManagerTabs { - display: flex; - align-items: center; - gap: 6px; - min-height: 52px; - padding: 8px 20px 0; - border-bottom: 1px solid var(--monitor-line); -} - -.priceManagerTab { - position: relative; - display: inline-flex; - align-items: center; - gap: 8px; - align-self: stretch; - min-width: 112px; - padding: 0 14px 7px; - border: 0; - background: transparent; - color: var(--monitor-muted); - font-size: 13px; - font-weight: 700; - cursor: pointer; -} - -.priceManagerTab::after { - content: ''; - position: absolute; - right: 10px; - bottom: -1px; - left: 10px; - height: 2px; - border-radius: 2px 2px 0 0; - background: transparent; -} - -.priceManagerTab > span { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 22px; - height: 20px; - padding: 0 6px; - border-radius: 999px; - background: var(--monitor-surface-muted); - color: var(--monitor-muted); - font-size: 11px; -} - -.priceManagerTab:hover { - color: var(--monitor-ink); -} - -.priceManagerTabActive { - color: var(--monitor-accent); -} - -.priceManagerTabActive::after { - background: var(--monitor-accent); -} - -.priceManagerTabActive > span { - background: color-mix(in srgb, var(--monitor-accent) 13%, var(--monitor-surface)); - color: var(--monitor-accent); -} - -.priceRuleWorkspace { - display: grid; - grid-template-columns: 260px minmax(0, 1fr); - height: min(64vh, 560px); - min-height: 0; - overflow: hidden; -} - -.priceRuleSidebar { - display: grid; - grid-template-rows: auto minmax(0, 1fr); - min-width: 0; - overflow: hidden; - border-right: 1px solid var(--monitor-line); - background: color-mix(in srgb, var(--monitor-surface-muted) 68%, var(--monitor-surface)); -} - -.priceRuleSearch { - position: relative; - padding: 14px; - border-bottom: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); -} - -.priceRuleSearch > svg { - position: absolute; - z-index: 1; - top: 50%; - left: 26px; - color: var(--monitor-muted); - pointer-events: none; - transform: translateY(-50%); -} - -.priceRuleSearch :global(.form-group) { - margin: 0; -} - -.priceRuleSearch input { - min-height: 38px; - padding-left: 34px; - background: var(--monitor-surface); -} - -.priceRuleList { - min-height: 0; - overflow: auto; - overscroll-behavior: contain; - padding: 8px; -} - -.priceRuleListItem { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: 10px; - width: 100%; - min-height: 58px; - padding: 10px 11px; - border: 1px solid transparent; - border-radius: 8px; - background: transparent; - color: var(--monitor-ink); - text-align: left; - cursor: pointer; -} - -.priceRuleListItem:hover { - background: var(--monitor-surface-hover); -} - -.priceRuleListItemActive { - border-color: color-mix(in srgb, var(--monitor-accent) 32%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-accent) 9%, var(--monitor-surface)); -} - -.priceRuleListIdentity, -.priceRuleListMeta { - display: grid; - gap: 4px; - min-width: 0; -} - -.priceRuleListIdentity strong, -.priceUnmatchedList strong { - overflow: hidden; - font-size: 13px; - line-height: 1.3; - text-overflow: ellipsis; - white-space: nowrap; -} - -.priceRuleListIdentity small, -.priceRuleListMeta small, -.priceUnmatchedList small { - overflow: hidden; - color: var(--monitor-muted); - font-size: 11px; - line-height: 1.3; - text-overflow: ellipsis; - white-space: nowrap; -} - -.priceRuleListMeta { - justify-items: end; - text-align: right; -} - -.priceRuleConfigured, -.priceRuleUnconfigured { - display: inline-flex; - align-items: center; - width: max-content; - min-height: 20px; - padding: 0 7px; - border-radius: 999px; - font-size: 10px; - font-weight: 800; - white-space: nowrap; -} - -.priceRuleConfigured { - background: color-mix(in srgb, var(--monitor-green) 13%, var(--monitor-surface)); - color: color-mix(in srgb, var(--monitor-green) 82%, var(--monitor-ink)); -} - -.priceRuleUnconfigured { - background: color-mix(in srgb, var(--monitor-amber) 14%, var(--monitor-surface)); - color: color-mix(in srgb, var(--monitor-amber) 76%, var(--monitor-ink)); -} - -.priceRuleListEmpty, -.priceRuleEditorEmpty { - display: grid; - place-items: center; - min-height: 140px; - padding: 24px; - color: var(--monitor-muted); - font-size: 13px; - text-align: center; -} - -.priceRuleEditorEmpty { - grid-row: 1 / -1; -} - -.priceRuleEditorPane { - display: grid; - grid-template-rows: auto minmax(0, 1fr) auto; - min-width: 0; - min-height: 0; - overflow: hidden; -} - -.priceRuleEditorHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - min-height: 72px; - padding: 14px 20px; - border-bottom: 1px solid var(--monitor-line); -} - -.priceRuleEditorHeader > div:first-child { - display: grid; - gap: 3px; - min-width: 0; -} - -.priceRuleEditorHeader h3, -.priceSyncHeader h3 { - overflow: hidden; - margin: 0; - color: var(--monitor-ink); - font-size: 16px; - font-weight: 750; - line-height: 1.3; - text-overflow: ellipsis; - white-space: nowrap; -} - -.priceRuleEditorHeader > div:first-child > span, -.priceSyncHeader > div:first-child > div > span { - color: var(--monitor-muted); - font-size: 12px; -} - -.priceRuleEditorBadges { - display: flex; - flex: 0 0 auto; - align-items: center; - gap: 6px; -} - -.priceRuleEditorBadges > span:not(.priceRuleConfigured):not(.priceRuleUnconfigured) { - display: inline-flex; - align-items: center; - min-height: 20px; - padding: 0 7px; - border: 1px solid var(--monitor-line); - border-radius: 999px; - color: var(--monitor-muted); - font-size: 10px; - font-weight: 700; -} - -.priceRuleEditorScroll { - min-height: 0; - overflow: auto; - overscroll-behavior: contain; -} - -.priceRuleSection { - display: grid; - gap: 16px; - padding: 20px; - border-bottom: 1px solid var(--monitor-line); -} - -.priceRuleSection:last-child { - border-bottom: 0; -} - -.priceRuleSectionHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.priceRuleSectionHeader > div { - display: flex; - align-items: baseline; - gap: 8px; - min-width: 0; -} - -.priceRuleSectionHeader h4 { - margin: 0; - color: var(--monitor-ink); - font-size: 13px; - font-weight: 800; -} - -.priceRuleSectionHeader span { - color: var(--monitor-muted); - font-size: 11px; - font-weight: 650; -} - -.priceBaseGrid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.priceTierList { - display: grid; - gap: 8px; -} - -.priceTierCompactRow { - display: grid; - grid-template-columns: 26px minmax(130px, 1.3fr) repeat(4, minmax(90px, 1fr)) 32px; - align-items: end; - gap: 8px; - padding: 10px; - border: 1px solid var(--monitor-line); - border-radius: 8px; - background: color-mix(in srgb, var(--monitor-surface-muted) 64%, var(--monitor-surface)); -} - -.priceTierCompactRow label { - display: grid; - gap: 6px; - min-width: 0; -} - -.priceTierCompactRow label > span { - display: flex; - align-items: flex-end; - min-height: 24px; - color: var(--monitor-muted); - font-size: 10px; - font-weight: 700; - line-height: 1.2; -} - -.priceTierCompactRow :global(.form-group) { - margin: 0; -} - -.priceTierCompactRow input { - min-height: 36px; - padding-inline: 9px; - font-size: 12px; -} - -.priceTierIndex { - display: inline-flex; - align-items: center; - justify-content: center; - align-self: end; - width: 24px; - height: 36px; - color: var(--monitor-muted); - font-size: 11px; - font-weight: 800; -} - -.priceTierRemoveButton { - display: inline-flex; - align-items: center; - justify-content: center; - align-self: end; - width: 32px; - height: 36px; - padding: 0; - border: 1px solid transparent; - border-radius: 7px; - background: transparent; - color: var(--monitor-muted); - cursor: pointer; -} - -.priceTierRemoveButton:hover { - border-color: color-mix(in srgb, var(--monitor-red) 20%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-red) 8%, var(--monitor-surface)); - color: var(--monitor-red); -} - -.priceTierEmpty { - display: grid; - place-items: center; - min-height: 82px; - padding: 16px; - border: 1px dashed var(--monitor-line); - border-radius: 8px; - color: var(--monitor-muted); - font-size: 12px; - text-align: center; -} - -.priceRuleEditorFooter { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - min-height: 64px; - padding: 12px 20px; - border-top: 1px solid var(--monitor-line); - background: var(--monitor-surface); -} - -.priceRuleEditorFooter > div { - display: flex; - align-items: center; - gap: 8px; -} - -.priceSyncView { - display: grid; - grid-auto-rows: max-content; - align-content: start; - gap: 16px; - min-height: 0; - max-height: min(64vh, 560px); - overflow: auto; - padding: 20px; -} - -.priceSyncHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; - min-height: 52px; -} - -.priceSyncHeader > div:first-child { - display: flex; - align-items: center; - gap: 12px; - min-width: 0; -} - -.priceSyncHeader > div:first-child > div { - display: grid; - gap: 4px; - min-width: 0; -} - -.priceSyncStatusDot { - flex: 0 0 auto; - width: 10px; - height: 10px; - border-radius: 999px; - background: var(--monitor-muted); - box-shadow: 0 0 0 4px color-mix(in srgb, var(--monitor-muted) 12%, transparent); -} - -.priceSyncStatussyncing { - background: var(--monitor-accent); - box-shadow: 0 0 0 4px color-mix(in srgb, var(--monitor-accent) 14%, transparent); - animation: monitor-pulse 1.4s ease-in-out infinite; -} - -.priceSyncStatussuccess { - background: var(--monitor-green); - box-shadow: 0 0 0 4px color-mix(in srgb, var(--monitor-green) 14%, transparent); -} - -.priceSyncStatuserror { - background: var(--monitor-red); - box-shadow: 0 0 0 4px color-mix(in srgb, var(--monitor-red) 14%, transparent); -} - -.priceSyncActions { - display: flex; - flex: 0 0 auto; - align-items: center; - gap: 8px; -} - -.priceSyncActions :global(.btn) { - min-height: 38px; -} - -.priceSyncApplyButton:global(.btn-primary) { - border-color: #2563eb; - background: #2563eb; - color: #fff; -} - -.priceSyncApplyButton:global(.btn-primary):hover:not(:disabled) { - border-color: #1d4ed8; - background: #1d4ed8; - color: #fff; -} - -.priceSyncApplyButton:global(.btn-primary):disabled { - border-color: var(--monitor-line-strong); - background: var(--monitor-surface-muted); - color: var(--monitor-muted); - opacity: 1; -} - -.priceSyncApplyButton:global(.btn) > span { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 7px; - color: inherit; - line-height: 1; -} - -.priceSyncApplyButton:global(.btn) > span svg, -.priceSyncApplyIcon { - display: block; - flex: 0 0 auto; - color: inherit; - fill: none; - stroke: currentColor; -} - -.priceSyncMetrics { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - grid-auto-rows: minmax(78px, max-content); - min-height: 80px; - overflow: hidden; - border: 1px solid var(--monitor-line); - border-radius: 8px; - background: var(--monitor-surface); -} - -.priceSyncMetrics > div { - display: flex; - flex-direction: column; - justify-content: center; - gap: 6px; - min-height: 78px; - padding: 11px 14px; - border-right: 1px solid var(--monitor-line); - background: color-mix(in srgb, var(--monitor-surface-elevated) 46%, var(--monitor-surface)); -} - -.priceSyncMetrics > div:last-child { - border-right: 0; -} - -.priceSyncMetrics span { - color: var(--monitor-muted); - font-size: 11px; - font-weight: 700; -} - -.priceSyncMetrics strong { - color: var(--monitor-ink); - font-size: 22px; - font-weight: 750; - font-variant-numeric: tabular-nums; - line-height: 1.2; -} - -.priceSyncMetricadded strong { - color: var(--monitor-green); -} - -.priceSyncMetricupdated strong { - color: var(--monitor-accent); -} - -.priceSyncMetricunmatched strong { - color: var(--monitor-amber); -} - -.priceSyncError { - padding: 11px 13px; - border: 1px solid color-mix(in srgb, var(--monitor-red) 24%, var(--monitor-line)); - border-radius: 8px; - background: color-mix(in srgb, var(--monitor-red) 7%, var(--monitor-surface)); - color: var(--monitor-red); - font-size: 12px; - line-height: 1.5; -} - -.priceSyncSchedule { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding-top: 14px; - border-top: 1px solid var(--monitor-line); -} - -.priceSyncSchedule > div:first-child { - display: grid; - gap: 3px; - min-width: 0; -} - -.priceSyncSchedule > div:first-child strong { - color: var(--monitor-ink); - font-size: 12px; - font-weight: 800; -} - -.priceSyncSchedule > div:first-child span, -.priceSyncScheduleInterval > span { - color: var(--monitor-muted); - font-size: 10px; - line-height: 1.35; -} - -.priceSyncScheduleControls { - display: flex; - flex: 0 0 auto; - align-items: flex-end; - gap: 10px; -} - -.priceSyncScheduleToggle { - display: inline-flex; - align-items: center; - gap: 7px; - color: var(--monitor-ink); - font-size: 11px; - font-weight: 700; - min-height: 36px; - white-space: nowrap; -} - -.priceSyncScheduleToggle input { - width: 16px; - height: 16px; - accent-color: var(--monitor-accent); -} - -.priceSyncScheduleInterval { - display: grid; - gap: 4px; - width: 138px; -} - -.priceSyncScheduleInterval :global(.form-group) { - margin: 0; -} - -.priceSyncScheduleInterval input { - min-height: 34px; -} - -.priceSyncChangesSection { - display: grid; - gap: 10px; - min-width: 0; -} - -.priceSyncChangesHeader { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 16px; -} - -.priceSyncChangesHeader > div:first-child { - display: grid; - gap: 3px; - min-width: 0; -} - -.priceSyncChangesHeader h4 { - margin: 0; - color: var(--monitor-ink); - font-size: 13px; - font-weight: 800; -} - -.priceSyncChangesHeader > div:first-child > span { - color: var(--monitor-muted); - font-size: 11px; -} - -.priceSyncChangesToolbar { - display: flex; - flex: 1 1 auto; - align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - gap: 8px; - min-width: 0; -} - -.priceSyncOverrideAll, -.priceSyncOverrideOption { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--monitor-muted); - font-size: 10px; - font-weight: 700; - white-space: nowrap; - cursor: pointer; -} - -.priceSyncOverrideAll { - min-height: 34px; - padding: 0 9px; - border: 1px solid var(--monitor-line); - border-radius: 8px; - background: var(--monitor-surface-muted); -} - -.priceSyncOverrideAll:has(input:checked) { - border-color: color-mix(in srgb, var(--monitor-accent) 34%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-accent) 8%, var(--monitor-surface)); - color: var(--monitor-accent); -} - -.priceSyncOverrideAll input, -.priceSyncOverrideOption input { - width: 15px; - height: 15px; - margin: 0; - accent-color: var(--monitor-accent); -} - -.priceSyncChangeFilters { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - gap: 3px; - padding: 3px; - border: 1px solid var(--monitor-line); - border-radius: 8px; - background: var(--monitor-surface-muted); -} - -.priceSyncChangeFilters button { - display: inline-flex; - align-items: center; - gap: 5px; - min-height: 30px; - padding: 0 8px; - border: 0; - border-radius: 6px; - background: transparent; - color: var(--monitor-muted); - font-size: 10px; - font-weight: 750; - line-height: 1; - white-space: nowrap; - cursor: pointer; -} - -.priceSyncChangeFilterLabel { - display: inline-flex; - align-items: center; - height: 18px; - line-height: 18px; -} - -.priceSyncChangeFilterCount { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 18px; - height: 18px; - padding: 0 5px; - border-radius: 999px; - background: color-mix(in srgb, var(--monitor-muted) 10%, transparent); - color: inherit; - font-size: 9px; - font-variant-numeric: tabular-nums; - line-height: 18px; -} - -.priceSyncChangeFilters .priceSyncChangeFilterActive { - background: var(--monitor-surface); - color: var(--monitor-ink); - box-shadow: 0 1px 3px color-mix(in srgb, var(--monitor-ink) 12%, transparent); -} - -.priceSyncChangeFilters .priceSyncChangeFilterActive .priceSyncChangeFilterCount { - background: color-mix(in srgb, var(--monitor-accent) 12%, var(--monitor-surface)); - color: var(--monitor-accent); -} - -.priceSyncChangeList { - display: grid; - overflow: hidden; - border: 1px solid var(--monitor-line); - border-radius: 8px; - background: var(--monitor-surface); -} - -.priceSyncChangeRow { - display: grid; - grid-template-columns: minmax(220px, 0.8fr) minmax(0, 1.2fr); - align-items: center; - gap: 16px; - min-height: 62px; - padding: 10px 12px; - border-bottom: 1px solid var(--monitor-line); -} - -.priceSyncChangeRow:last-child { - border-bottom: 0; -} - -.priceSyncChangeIdentity { - display: flex; - align-items: center; - gap: 9px; - min-width: 0; -} - -.priceSyncChangeIdentity > div { - display: grid; - gap: 3px; - min-width: 0; -} - -.priceSyncChangeIdentity strong { - overflow: hidden; - color: var(--monitor-ink); - font-size: 12px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.priceSyncChangeIdentity small, -.priceSyncChangeHint, -.priceSyncRateChanges > small { - overflow: hidden; - color: var(--monitor-muted); - font-size: 10px; - line-height: 1.35; - text-overflow: ellipsis; - white-space: nowrap; -} - -.priceSyncChangeBadge { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - min-width: 46px; - min-height: 22px; - padding: 0 7px; - border-radius: 999px; - font-size: 9px; - font-weight: 800; -} - -.priceSyncChangeadded { - background: color-mix(in srgb, var(--monitor-green) 13%, var(--monitor-surface)); - color: var(--monitor-green); -} - -.priceSyncChangeupdated { - background: color-mix(in srgb, var(--monitor-accent) 12%, var(--monitor-surface)); - color: var(--monitor-accent); -} - -.priceSyncChangeoverridden { - background: color-mix(in srgb, var(--monitor-accent) 16%, var(--monitor-surface)); - color: color-mix(in srgb, var(--monitor-accent) 82%, var(--monitor-ink)); -} - -.priceSyncChangelocked { - background: color-mix(in srgb, var(--monitor-muted) 12%, var(--monitor-surface)); - color: var(--monitor-muted); -} - -.priceSyncChangeunmatched { - background: color-mix(in srgb, var(--monitor-amber) 14%, var(--monitor-surface)); - color: color-mix(in srgb, var(--monitor-amber) 78%, var(--monitor-ink)); -} - -.priceSyncRateChanges { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: flex-end; - gap: 6px 14px; - min-width: 0; -} - -.priceSyncRateChanges > div { - display: grid; - gap: 2px; - min-width: 84px; -} - -.priceSyncRateChanges > div > span { - color: var(--monitor-muted); - font-size: 9px; -} - -.priceSyncRateChanges > div > div { - display: flex; - align-items: baseline; - gap: 5px; - font-size: 10px; -} - -.priceSyncRateChanges del { - color: var(--monitor-muted); -} - -.priceSyncRateChanges strong { - color: var(--monitor-ink); - font-size: 11px; -} - -.priceSyncOverrideOption { - min-height: 30px; - padding: 0 8px; - border: 1px solid var(--monitor-line); - border-radius: 7px; - background: var(--monitor-surface-muted); - color: var(--monitor-ink); -} - -.priceSyncOverrideOption:has(input:checked) { - border-color: color-mix(in srgb, var(--monitor-accent) 34%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-accent) 8%, var(--monitor-surface)); - color: var(--monitor-accent); -} - -.priceSyncChangesEmpty { - display: grid; - place-items: center; - min-height: 72px; - padding: 14px; - color: var(--monitor-muted); - font-size: 11px; - text-align: center; -} - -.priceSyncResultSection { - display: grid; - gap: 12px; -} - -.priceUnmatchedList { - display: grid; - overflow: hidden; - border: 1px solid var(--monitor-line); - border-radius: 8px; -} - -.priceUnmatchedList > div:not(.priceTierEmpty) { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - min-height: 54px; - padding: 9px 13px; - border-bottom: 1px solid var(--monitor-line); -} - -.priceUnmatchedList > div:last-child { - border-bottom: 0; -} - -.priceUnmatchedList > .priceTierEmpty { - border: 0; - border-radius: 0; -} - -.priceUnmatchedList > div > span { - display: grid; - gap: 3px; - min-width: 0; -} - -.tableWrapper { - position: relative; - min-width: 0; - max-width: 100%; - overflow-x: auto; - scrollbar-gutter: stable; -} - -.tableScrollWrapper { - max-height: min(560px, 64vh); - overflow: auto; - overscroll-behavior: contain; - padding-right: 0; - border: 1px solid var(--monitor-line); - border-radius: 8px; -} - -.accountTableWrapper { - max-height: min(520px, 58vh); -} - -.realtimeTableWrapper { - height: 100%; - max-height: none; - overflow-x: auto; - overflow-y: auto; - overflow-anchor: none; - touch-action: pan-x pan-y; - -webkit-overflow-scrolling: touch; -} - -.tableScrollWrapper::-webkit-scrollbar { - width: 10px; - height: 10px; -} - -.tableScrollWrapper::-webkit-scrollbar-thumb { - border: 3px solid transparent; - border-radius: 999px; - background: color-mix(in srgb, var(--monitor-muted) 34%, transparent); - background-clip: padding-box; -} - -.tableScrollWrapper::-webkit-scrollbar-track { - background: transparent; -} - -.tableWrapper::after { - content: ''; - position: sticky; - right: 0; - display: block; - width: 36px; - height: 1px; - margin-top: -1px; - margin-left: auto; - pointer-events: none; - background: linear-gradient(90deg, transparent, var(--monitor-surface)); -} - -.table { - width: 100%; - min-width: 0; - border-collapse: collapse; - border-spacing: 0; -} - -.accountOverviewTable { - --account-col-account: 24%; - --account-col-total-calls: 9%; - --account-col-success-calls: 10%; - --account-col-success-rate: 9%; - --account-col-total-tokens: 12%; - --account-col-estimated-cost: 10%; - --account-col-latest-request: 17%; - --account-col-action: 9%; - --account-overview-columns: - var(--account-col-account) - var(--account-col-total-calls) - var(--account-col-success-calls) - var(--account-col-success-rate) - var(--account-col-total-tokens) - var(--account-col-estimated-cost) - var(--account-col-latest-request) - var(--account-col-action); - - min-width: 0; - table-layout: fixed; -} - -.accountOverviewTable col:nth-child(1) { - width: var(--account-col-account); -} - -.accountOverviewTable col:nth-child(2) { - width: var(--account-col-total-calls); -} - -.accountOverviewTable col:nth-child(3) { - width: var(--account-col-success-calls); -} - -.accountOverviewTable col:nth-child(4) { - width: var(--account-col-success-rate); -} - -.accountOverviewTable col:nth-child(5) { - width: var(--account-col-total-tokens); -} - -.accountOverviewTable col:nth-child(6) { - width: var(--account-col-estimated-cost); -} - -.accountOverviewTable col:nth-child(7) { - width: var(--account-col-latest-request); -} - -.accountOverviewTable col:nth-child(8) { - width: var(--account-col-action); -} - -.accountOverviewTable .accountButtonLabel { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; -} - -.accountOverviewTable .accountButton small { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.accountOverviewTable td:nth-child(7), -.accountOverviewTable th:nth-child(7) { - white-space: normal; -} - -.accountOverviewTable td:nth-child(7) { - font-size: 12px; - line-height: 1.35; -} - -.accountOverviewTable td:last-child { - min-width: 0; -} - -.accountOverviewTable td:last-child .inlineActionButton { - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.realtimeTable { - width: var(--realtime-table-min-width, max-content); - min-width: var(--realtime-table-min-width, max-content); - table-layout: fixed; -} - -.table thead th { - position: sticky; - top: 0; - z-index: 3; - padding: 10px 12px; - background: var(--monitor-surface-muted); - text-align: left; - color: var(--monitor-ink); - font-size: 12px; - font-weight: 700; - letter-spacing: 0; - text-transform: none; - white-space: normal; - line-height: 1.35; -} - -.table thead th::after { - content: ''; - position: absolute; - right: 0; - bottom: 0; - left: 0; - height: 1px; - background: var(--monitor-line); -} - -.sortableHeaderButton { - width: 100%; - display: inline-flex; - align-items: center; - justify-content: flex-start; - gap: 3px; - min-width: 0; - padding: 0; - border: 0; - background: transparent; - color: inherit; - font: inherit; - letter-spacing: inherit; - text-align: left; - text-transform: inherit; - cursor: pointer; -} - -.sortableHeaderButton:hover { - transform: none; - color: var(--monitor-ink); -} - -.sortableHeaderButton:focus, -.sortableHeaderButton:focus-visible { - outline: none; - box-shadow: none; -} - -.sortableHeaderButtonActive { - color: var(--monitor-ink); -} - -.sortIndicator { - display: inline-flex; - align-items: center; - justify-content: center; - width: 14px; - min-width: 14px; - height: 14px; - color: var(--monitor-accent-strong); -} - -.sortIndicator svg { - display: block; -} - -.table tbody td { - min-width: 0; - padding: 10px 12px; - vertical-align: top; - white-space: nowrap; - border-top: 1px solid var(--monitor-line); - background: var(--monitor-surface); - font-size: 13px; - line-height: 1.45; -} - -.realtimeTable tbody td { - vertical-align: middle; -} - -.table tbody td:first-child { - border-left: 0; - border-radius: 0; -} - -.table tbody td:last-child { - border-right: 0; -} - -//.table tbody tr:hover td { -// background: var(--monitor-surface-hover); -//} - -.table tbody tr:nth-child(2n) td { - background: color-mix(in srgb, var(--monitor-surface-muted) 72%, var(--monitor-surface)); -} - -.innerTable { - min-width: 940px; -} - -.primaryCell { - display: grid; - gap: 4px; - min-width: 0; -} - -.primaryCell span, -.primaryCell small { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - line-height: 1.35; -} - -.primaryCell span { - font-size: 13px; - font-weight: 600; -} - -.realtimeTimeCell { - white-space: normal; - line-height: 1.35; - font-size: 12px; - color: var(--monitor-ink); -} - -.realtimeNowrapCell { - white-space: nowrap; -} - -.realtimeMetricCell { - text-align: left; -} - -.table thead th.realtimeMetricHeader { - text-align: left; -} - -.table thead th.realtimeCenterHeader, -.realtimeCenterCell { - text-align: left; -} - -.realtimeCenterCell > * { - margin-inline: 0; -} - -.realtimeCostCell { - display: inline-flex; - align-items: center; - justify-content: flex-start; - gap: 6px; - width: 100%; - min-height: 54px; - font-variant-numeric: tabular-nums; -} - -.realtimeCostValue { - overflow: visible; - color: var(--monitor-green); - font-weight: 750; - line-height: 1.2; - text-overflow: clip; -} - -.realtimeCostInfoButton { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - padding: 0; - border: 0; - border-radius: 50%; - background: transparent; - color: var(--monitor-muted); - cursor: help; -} - -.realtimeCostInfoButton:hover, -.realtimeCostInfoButton:focus-visible { - background: color-mix(in srgb, var(--monitor-accent) 10%, transparent); - color: var(--monitor-accent-strong); - outline: none; -} - -.realtimeCostInfoButton svg { - display: block; -} - -.realtimeCostTooltip { - position: fixed; - z-index: 12000; - width: min(336px, calc(100vw - 24px)); - max-height: min(420px, calc(100vh - 24px)); - overflow: visible; - padding: 18px 20px; - border: 1px solid #334155; - border-radius: 12px; - background: #060b1a; - color: #f8fafc; - box-shadow: 0 20px 48px -20px rgba(2, 6, 23, 0.72); - pointer-events: none; -} - -.realtimeCostTooltip::before { - content: ''; - position: absolute; - top: var(--realtime-cost-arrow-top, 50%); - width: 14px; - height: 14px; - border: solid #334155; - background: #060b1a; - transform: translateY(-50%) rotate(45deg); -} - -.realtimeCostTooltip[data-placement='left']::before { - right: -8px; - border-width: 1px 1px 0 0; -} - -.realtimeCostTooltip[data-placement='right']::before { - left: -8px; - border-width: 0 0 1px 1px; -} - -.realtimeCostTooltipTitle { - display: block; - margin-bottom: 12px; - color: #cbd5e1; - font-size: 15px; - font-weight: 800; - line-height: 1.2; -} - -.realtimeCostTooltipRows { - display: grid; - gap: 9px; - max-height: min(340px, calc(100vh - 90px)); - overflow: auto; - padding-right: 2px; -} - -.realtimeCostTooltipRows > div:not(.realtimeCostTooltipDivider) { - display: grid; - grid-template-columns: minmax(0, 1fr) max-content; - align-items: baseline; - gap: 14px; -} - -.realtimeCostTooltipRows span { - color: #94a3b8; - font-size: 12px; - line-height: 1.35; -} - -.realtimeCostTooltipRows strong { - color: #f8fafc; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-size: 12px; - font-weight: 600; - letter-spacing: 0; - line-height: 1.35; - text-align: right; - white-space: nowrap; -} - -.realtimeCostTooltipRows .realtimeCostRateInput { - color: #38bdf8; -} - -.realtimeCostTooltipRows .realtimeCostRateOutput { - color: #c4b5fd; -} - -.realtimeCostTooltipDivider { - height: 1px; - margin: 2px 0; - background: #1e293b; -} - -.realtimeCostTooltipEmpty { - margin: 0; - color: #94a3b8; - font-size: 12px; - line-height: 1.55; -} - -.realtimeDraggableHeader { - position: relative; - white-space: nowrap; - cursor: grab; -} - -.realtimeDraggableHeader:active { - cursor: grabbing; -} - -.realtimeFixedHeader, -.realtimeFixedHeader:active { - cursor: default; -} - -.realtimeDraggableHeaderActive { - color: var(--monitor-accent-strong); - background: color-mix(in srgb, var(--monitor-accent) 14%, var(--monitor-surface-muted)); -} - -.realtimeHeaderContent { - display: inline-block; - max-width: calc(100% - 12px); - overflow: visible; - white-space: nowrap; - vertical-align: middle; -} - -.realtimeColumnResizeHandle { - position: absolute; - top: 0; - right: -4px; - bottom: 0; - z-index: 4; - width: 8px; - cursor: col-resize; - touch-action: none; -} - -.realtimeColumnResizeHandle::after { - content: ''; - position: absolute; - top: 9px; - right: 3px; - bottom: 9px; - width: 1px; - background: transparent; -} - -.realtimeColumnResizeHandle:hover::after { - background: var(--monitor-accent-strong); -} - -.realtimeMetricCell { - vertical-align: middle; - white-space: nowrap; -} - -.realtimeReasoningCol { - min-width: 96px; -} - -.realtimeStreamCol { - min-width: 92px; -} - -.realtimeReasoningBadge { - display: inline-flex; - max-width: 100%; -} - -.realtimeTokensTableCell { - white-space: normal; -} - -.realtimeCacheReadTableCell { - text-align: left; - white-space: normal; -} - -.realtimeStatusErrorButton { - display: inline-flex; - align-items: center; - justify-content: flex-start; - width: 100%; - min-width: 0; - padding: 0; - border: 0; - border-radius: 999px; - background: transparent; - color: inherit; - cursor: pointer; -} - -.realtimeStatusErrorButton:hover { - transform: none; - filter: brightness(0.97); -} - -.realtimeStatusErrorButton:focus-visible { - outline: 2px solid color-mix(in srgb, var(--monitor-red) 42%, var(--monitor-line)); - outline-offset: 2px; - border-radius: 999px; -} - -.realtimeStatusErrorButton .statusBadge { - width: 100%; -} - -.realtimeErrorDetailsPanel { - display: grid; - gap: 14px; - padding: 2px 0 4px; -} - -.monitorModalActions { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 8px; -} - -.realtimeErrorOverview { - display: grid; - gap: 10px; - min-width: 0; - padding: 14px 16px; - border: 1px solid color-mix(in srgb, var(--monitor-red) 24%, var(--monitor-line)); - border-radius: 10px; - background: color-mix(in srgb, var(--monitor-red) 7%, var(--monitor-surface)); -} - -.realtimeErrorOverviewTop { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - min-width: 0; -} - -.realtimeErrorOverviewTop .statusBadge { - width: auto; -} - -.realtimeErrorOverviewTop span { - color: var(--monitor-red); - font-size: 12px; - font-weight: 650; - line-height: 1.35; -} - -.realtimeErrorOverview strong { - color: var(--monitor-ink); - font-size: 15px; - font-weight: 650; - line-height: 1.45; - overflow-wrap: anywhere; -} - -.realtimeErrorMessageBlock { - display: grid; - gap: 6px; -} - -.realtimeErrorMessageBlock > span { - color: var(--monitor-muted); - font-size: 12px; - font-weight: 600; - line-height: 1.35; -} - -.realtimeErrorMessage { - max-height: 180px; - overflow: auto; - margin: 0; - padding: 12px 14px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 86%, transparent); - border-radius: 8px; - background: color-mix(in srgb, var(--monitor-surface-muted) 58%, var(--monitor-surface)); - color: var(--monitor-ink); - font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); - font-size: 12px; - line-height: 1.5; - white-space: pre-wrap; -} - -.realtimeErrorDetailsGrid { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 0; - border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); - border-radius: 8px; - overflow: hidden; -} - -.realtimeErrorDetailItem { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 16px; - min-width: 0; - padding: 9px 12px; - background: var(--monitor-surface); -} - -.realtimeErrorDetailItem + .realtimeErrorDetailItem { - border-top: 1px solid color-mix(in srgb, var(--monitor-line) 72%, transparent); -} - -.realtimeErrorDetailItem span { - flex: 0 0 auto; - color: var(--monitor-muted); - font-size: 12px; - line-height: 1.35; -} - -.realtimeErrorDetailItem strong { - min-width: 0; - max-width: 72%; - text-align: right; - overflow-wrap: anywhere; - color: var(--monitor-ink); - font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); - font-size: 12px; - font-weight: 600; - line-height: 1.35; -} - -.realtimeTokenCell { - width: 100%; - min-height: 54px; - align-content: center; - gap: 3px; -} - -.realtimeTokenCell > span, -.realtimeTokenCell > small { - display: block; - overflow: visible; - text-overflow: clip; - white-space: nowrap; -} - -.realtimeTokenCell > span { - color: var(--monitor-muted); - font-size: 12px; - font-weight: 700; -} - -.realtimeTokenCell > span strong { - color: var(--monitor-ink); - font-size: 15px; - font-weight: 800; - font-variant-numeric: tabular-nums; -} - -.realtimeTokenCell > small { - color: var(--monitor-muted); - font-size: 11px; - font-weight: 600; -} - -.realtimeCacheReadCell { - display: grid; - align-content: center; - justify-items: start; - width: 100%; - min-height: 54px; - gap: 2px; - font-variant-numeric: tabular-nums; -} - -.realtimeCacheReadCell strong { - color: var(--monitor-ink); - font-size: 15px; - font-weight: 800; - line-height: 1.3; -} - -.realtimeCacheReadCell small { - color: var(--monitor-muted); - font-size: 11px; - font-weight: 650; - line-height: 1.35; - white-space: nowrap; -} - -.realtimeCacheReadCell .realtimeCacheHitLow { - color: var(--monitor-red); -} - -.primaryCell small, -.statusCell small { - color: var(--monitor-muted); - font-size: 11px; -} - -.monoCell { - font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); -} - -.mutedText { - color: var(--monitor-muted); -} - -.statusCell { - display: grid; - gap: 6px; - min-width: 88px; -} - -.recentStatusCell { - display: flex; - align-items: center; - min-width: 70px; -} - -.realtimeTable .statusBadge { - min-height: 24px; - padding: 0 8px; - font-size: 12px; - justify-content: center; - text-align: center; - width: 100%; -} - -.realtimeTable .realtimeReasoningBadge .statusBadge { - width: auto; - min-width: 56px; - max-width: 100%; - padding-inline: 10px; -} - -.realtimeTable .realtimeNonStreamingBadge .statusBadge { - color: var(--monitor-cyan); - border-color: color-mix(in srgb, var(--monitor-cyan) 26%, var(--monitor-line)); - background: color-mix(in srgb, var(--monitor-cyan) 10%, var(--monitor-surface)); -} - -.realtimeTable .statusBadge::before { - content: ''; - width: 6px; - height: 6px; - border-radius: 50%; - background: currentColor; - flex-shrink: 0; -} - -.accountButton { - width: 100%; - display: grid; - grid-template-columns: 18px minmax(0, 1fr); - gap: 4px 8px; - align-items: start; - border: 0; - background: transparent; - color: inherit; - padding: 0; - min-width: 0; - text-align: left; - cursor: pointer; - white-space: normal; -} - -.accountButton:hover { - transform: none; - color: var(--monitor-accent-strong); -} - -.accountButton:focus, -.accountButton:focus-visible { - outline: none; - box-shadow: none; -} - -.accountButton:focus-visible { - color: var(--monitor-accent-strong); -} - -.accountButtonLabel { - min-width: 0; - font-size: 13px; - font-weight: 600; - line-height: 1.35; - overflow-wrap: anywhere; - word-break: break-word; -} - -.accountButton small { - grid-column: 2; - color: var(--monitor-muted); - font-size: 11px; - line-height: 1.35; - white-space: normal; - overflow-wrap: anywhere; - word-break: break-word; -} - -.expandedAccountButton { - align-items: start; -} - -.focusedRow td { - border-color: var(--monitor-line); - background: var(--monitor-surface-elevated); - box-shadow: none; -} - -.expandedAccountCardRow td { - padding: 8px 0; - border-top: 1px solid var(--monitor-line); - background: transparent; - white-space: normal; -} - -.expandedAccountCardRow td:first-child, -.expandedAccountCardRow td:last-child { - border-radius: 0; -} - -//.expandedAccountCardRow:hover td { -// background: transparent; -//} - -.expandedAccountCard { - display: grid; - gap: 16px; - min-width: 0; - border-radius: 12px; - border: 1px solid var(--monitor-line); - background: var(--monitor-surface-elevated); -} - -.expandedAccountSummary { - display: grid; - grid-template-columns: var(--account-overview-columns); - gap: 0; - align-items: center; - min-width: 0; -} - -.expandedAccountPrimary, -.expandedAccountBody { - min-width: 0; -} - -.expandedAccountPrimary, -.expandedAccountMetricValue { - display: flex; - align-items: center; - box-sizing: border-box; - padding: 14px 10px; - min-width: 0; -} - -.expandedAccountPrimary { - align-items: flex-start; -} - -.expandedAccountMetricValue strong { - font-size: 12px; - line-height: 1.35; -} - -.expandedAccountAction { - display: flex; - align-items: center; - justify-content: flex-end; - box-sizing: border-box; - padding: 14px 10px; - min-width: 0; -} - -.expandPanel { - display: grid; - gap: 16px; - min-width: 0; -} - -.quotaRefreshButton { - display: inline-flex; - align-items: center; - gap: 8px; - min-height: 34px; - padding: 0 12px; - border: 1px solid var(--monitor-line); - border-radius: 999px; - background: var(--monitor-surface); - color: var(--monitor-ink); - font: inherit; - font-size: 12px; - font-weight: 700; - cursor: pointer; - white-space: nowrap; -} - -.quotaRefreshButton:disabled { - cursor: not-allowed; - opacity: 0.72; - transform: none; -} - -.quotaSection { - display: grid; - gap: 12px; - padding: 0; - border: 0; - background: transparent; -} - -.quotaSectionHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.quotaSectionTitleGroup { - display: grid; - gap: 4px; -} - -.quotaSectionTitleGroup strong { - font-size: 14px; - line-height: 1.2; -} - -.quotaSectionTitleGroup span, -.quotaEntryMain small, -.quotaWindowRow small { - color: var(--monitor-muted); - font-size: 12px; -} - -.quotaEntryGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 12px; -} - -.quotaCompactCard { - display: grid; - gap: 14px; - min-width: 0; - padding: 14px 16px; - border-radius: 16px; - border: 0; - background: color-mix(in srgb, var(--monitor-accent) 6%, var(--monitor-surface-elevated)); -} - -.quotaCompactHeader { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - justify-content: space-between; - gap: 12px; -} - -.quotaEntryCard { - display: grid; - gap: 12px; - min-width: 0; - padding: 14px 16px; - border-radius: 14px; - border: 0; - background: var(--monitor-surface-elevated); -} - -.quotaEntryHeader, -.quotaEntryMain, -.quotaWindowList, -.quotaWindowRow { - display: grid; - gap: 8px; -} - -.quotaEntryMain strong, -.quotaWindowHeader strong { - font-size: 13px; - line-height: 1.3; -} - -.quotaWindowHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.quotaWindowHeader span { - color: var(--monitor-ink); - font-size: 12px; -} - -.quotaProgressTrack { - position: relative; - overflow: hidden; - height: 8px; - border-radius: 999px; - background: color-mix(in srgb, var(--monitor-line) 68%, transparent); -} - -.quotaProgressBar { - position: absolute; - inset: 0 auto 0 0; - border-radius: inherit; - background: linear-gradient(135deg, var(--monitor-accent), var(--monitor-cyan)); -} - -.quotaStateMessage { - display: grid; - min-height: 44px; - place-items: center start; - padding: 10px 12px; - border-radius: 14px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); - background: color-mix(in srgb, var(--monitor-surface-elevated) 82%, var(--monitor-surface)); - color: var(--monitor-muted); - font-size: 12px; -} - -.accountSectionHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - flex-wrap: wrap; - min-width: 0; -} - -.accountSectionHeader strong { - min-width: 0; - color: var(--monitor-ink); - font-size: 14px; - line-height: 1.25; -} - -.accountOverviewCard .accountButton { - align-items: center; -} - -.accountOverviewCard .accountButtonLabel { - overflow: hidden; - overflow-wrap: normal; - text-overflow: ellipsis; - white-space: nowrap; - word-break: normal; - line-height: 1.45; -} - -.accountOverviewCard .accountButton small { - overflow: hidden; - overflow-wrap: normal; - text-overflow: ellipsis; - white-space: nowrap; - word-break: normal; -} - -.accountOverviewCardBody .quotaRefreshButton { - gap: 6px; - min-height: 28px; - padding: 0 9px; - font-size: 11px; -} - -.accountOverviewCardBody .quotaSection { - align-content: start; - gap: 10px; - min-width: 0; - padding: 0; - border: 0; - border-radius: 0; - background: transparent; -} - -.accountOverviewCardBody .quotaSectionHeader { - gap: 8px; - flex-wrap: wrap; -} - -.accountOverviewCardBody .quotaEntryGrid { - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 10px; -} - -.accountOverviewCardBody .quotaEntryCard { - gap: 10px; - min-width: 0; - overflow: hidden; - padding: 12px; - border-radius: 10px; - border: 1px solid color-mix(in srgb, var(--monitor-line) 82%, transparent); - background: color-mix(in srgb, var(--monitor-surface) 78%, var(--monitor-surface-elevated)); -} - -.accountQuotaContent, -.accountQuotaRow, -.accountQuotaRowHeader, -.accountQuotaModel, -.accountQuotaMeta, -.accountQuotaPlanValue, -.accountQuotaPremiumPlanValue, -.accountQuotaResetCreditRow, -.accountQuotaResetCreditTime { - min-width: 0; - max-width: 100%; -} - -.accountQuotaContent, -.accountQuotaRow { - width: 100%; -} - -.accountQuotaRowHeader { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, max-content); - align-items: baseline; - gap: 8px; -} - -.accountQuotaModel { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.accountQuotaMeta { - display: flex; - align-items: baseline; - justify-content: flex-end; - flex-wrap: nowrap; - gap: 8px; - width: auto; - line-height: 1.4; - overflow: hidden; - white-space: nowrap; -} - -.accountQuotaAmount { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -.accountQuotaMeta > :not(.accountQuotaAmount) { - flex: 0 0 auto; -} - -.accountQuotaPlanValue { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.accountQuotaPremiumPlanValue { - overflow: hidden; - text-align: center; - text-overflow: ellipsis; - white-space: nowrap; -} - -.accountQuotaResetCreditRow { - align-items: flex-start; - flex-wrap: wrap; -} - -.accountQuotaResetCreditTime { - flex: 1 1 140px; - text-align: left; -} - -.realtimeTable .primaryCell small { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - white-space: normal; - overflow-wrap: anywhere; - line-height: 1.45; -} - -.realtimeTokenCell > span, -.realtimeTokenCell > small { - display: block; - white-space: nowrap; - overflow-wrap: normal; - word-break: normal; -} - -.realtimeTable .realtimeTokenCell > small { - display: block; - overflow: visible; - text-overflow: clip; - white-space: nowrap; - -webkit-box-orient: initial; - -webkit-line-clamp: unset; -} - -.logRowFailed td { - border-top-color: color-mix(in srgb, var(--monitor-red) 28%, var(--monitor-line)); - border-bottom-color: color-mix(in srgb, var(--monitor-red) 28%, var(--monitor-line)); -} - -.logRowFailed td:first-child, -.logRowFailed td:last-child { - border-color: color-mix(in srgb, var(--monitor-red) 28%, var(--monitor-line)); -} - -//.logRowFailed:hover td { -// background: color-mix(in srgb, var(--monitor-red) 5%, var(--monitor-surface-hover)); -//} - -.emptyBlock, -.emptyTable, -.emptyBlockSmall { - display: grid; - place-items: center; - color: var(--monitor-muted); - text-align: center; - font-size: 14px; -} - -.emptyBlock, -.emptyTable { - min-height: 120px; -} - -.emptyBlockSmall { - min-height: 80px; -} - -@keyframes monitor-spin { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } -} - -@keyframes monitor-pulse { - 0%, - 100% { - opacity: 1; - } - - 50% { - opacity: 0.45; - } -} - -@container monitoring-page (max-width: 760px) { - .usageStatsGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@container monitoring-page (max-width: 1120px) { - .toolbarControlRow { - flex-wrap: wrap; - justify-content: flex-start; - } - - .refreshCluster { - justify-content: flex-start; - } -} - -@container monitoring-page (max-width: 900px) { - .usageTrendHeader, - .usageTrendCollapsed { - grid-template-columns: minmax(0, 1fr) auto; - align-items: start; - gap: 10px 12px; - } - - .usageTrendCopy { - display: contents; - } - - .usageTrendCopy h2 { - grid-column: 1; - grid-row: 1; - align-self: center; - } - - .usageTrendHeader .usageTrendCopy p { - grid-column: 1 / -1; - grid-row: 2; - } - - .usageTrendActions { - grid-column: 1 / -1; - grid-row: 3; - width: 100%; - justify-content: flex-start; - } - - .usageTrendHeader > .usageTrendActions > .usageTrendHideButton { - display: none; - } - - .mobileHeaderHideButton { - display: inline-flex; - grid-column: 2; - align-self: center; - margin-top: 0; - transform: translateY(5px); - } - - .timeRangeControl, - .usageTrendRangeControl { - flex: 0 1 auto; - } - - .usageTrendApiKeySelect { - width: 150px; - } - - .usageTrendInsightsGrid, - .rankingGrid { - grid-template-columns: 1fr; - } - - .modelStatsLayout { - grid-template-columns: minmax(0, 1fr) minmax(210px, 240px); - } - - .settingsGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .accountOverviewTable { - min-width: 920px; - } - - .realtimeTable { - min-width: 1360px; - } - - .accountOverviewTable th:first-child, - .accountOverviewTable td:first-child { - position: sticky; - left: 0; - z-index: 2; - box-shadow: 8px 0 14px color-mix(in srgb, var(--monitor-bg) 56%, transparent); - } - - .accountOverviewTable th:first-child { - z-index: 4; - background: var(--monitor-surface); - } - - .accountOverviewTable td:first-child { - background: var(--monitor-surface-elevated); - } - -} - -@container monitoring-page (max-width: 680px) { - .realtimeLogStatusRow { - align-items: flex-start; - flex-direction: column; - } - - .realtimeUpdateBar { - align-items: flex-start; - flex-wrap: wrap; - } - - .usageTrendActions { - align-items: center; - overflow: visible; - } - - .timeRangeControl, - .usageTrendRangeControl { - flex: 0 1 auto; - min-width: 0; - width: auto; - overflow-x: auto; - scrollbar-width: none; - } - - .timeRangeControl::-webkit-scrollbar, - .usageTrendRangeControl::-webkit-scrollbar { - display: none; - } - - .timeRangeButton, - .usageTrendRangeButton { - flex: 0 0 auto; - min-width: 54px; - padding: 0 10px; - } - - .usageTrendApiKeySelect { - width: 132px; - } - - .usageTrendChartCard { - height: auto; - min-height: 520px; - padding: 16px; - } - - .professionalChartShell { - grid-template-rows: auto minmax(230px, 240px) auto; - gap: 16px; - align-content: start; - } - - .usageTrendSvg { - height: 240px; - } - - .trendChartLegend { - justify-content: flex-start; - } - - .accountStatsToolbar { - align-items: stretch; - flex-direction: column; - flex-wrap: nowrap; - } - - .accountStatsFilters { - display: grid; - grid-template-columns: minmax(112px, 1fr) minmax(104px, 0.9fr) minmax(96px, 0.8fr); - gap: 8px; - overflow-x: auto; - flex-wrap: nowrap; - scrollbar-width: none; - } - - .accountStatsFilters::-webkit-scrollbar { - display: none; - } - - .accountStatsSearchInput, - .accountStatsSelect { - width: 100%; - } - - .accountStatsToolbar > .rankingMetricSwitch { - width: max-content; - max-width: 100%; - overflow-x: auto; - justify-content: flex-start; - scrollbar-width: none; - } - - .accountStatsToolbar > .rankingMetricSwitch::-webkit-scrollbar { - display: none; - } - - .accountStatsToolbar > .rankingMetricSwitch .rankingMetricButton { - flex: 0 0 auto; - min-width: 58px; - padding: 0 12px; - } - - .accountStatsClearButton { - display: none; - } - - .accountOverviewCardGrid { - grid-template-columns: 1fr; - } - - .healthMetricGrid, - .accountOverviewMetricGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .trendCardHeader { - flex-direction: column; - } - - .trendSummaryStrip { - grid-template-columns: repeat(4, minmax(96px, 1fr)); - overflow-x: auto; - scrollbar-width: none; - } - - .trendSummaryStrip::-webkit-scrollbar { - display: none; - } - - .tokenDistributionLegend { - justify-content: flex-start; - } - - .trendMetricStrip, - .tokenTypeSummaryGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .modelStatsLayout { - grid-template-columns: minmax(0, 1fr) minmax(190px, 220px); - } - - .modelStatsCard .modelSharePanel { - grid-template-columns: 1fr; - justify-items: center; - gap: 8px; - } - - .modelStatsCard .modelShareHeader, - .modelStatsCard .modelLegend, - .modelStatsCard .donutChart { - grid-column: auto; - grid-row: auto; - } - - .modelStatsCard .donutChart { - width: min(150px, 100%); - } -} - -@container monitoring-page (max-width: 520px) { - .usageStatsGrid { - grid-template-columns: 1fr; - } - - .rankingHeader { - flex-wrap: nowrap; - } - - .rankingMetricSwitch { - justify-content: flex-start; - max-width: 100%; - } - - .rankingMetricButton { - padding: 0 9px; - } - - .tokenDistributionHeader { - flex-direction: row; - align-items: flex-start; - } - - .tokenCostBadge { - min-width: 98px; - padding: 7px 9px; - } - - .trendSummaryStrip { - grid-template-columns: repeat(4, minmax(92px, 1fr)); - } - - .usageTrendChartCard { - min-height: 500px; - padding: 12px; - } - - .professionalChartShell { - grid-template-rows: auto minmax(210px, 220px) auto; - gap: 16px; - } - - .usageTrendSvg { - height: 220px; - } - - .trendSummaryItem { - padding: 10px 10px 10px 16px; - } - - .trendSummaryItem strong { - font-size: 17px; - } - - .trendChartLegend { - gap: 8px 14px; - } - - .trendChartLegend span { - font-size: 12px; - } - - .trendChartLegend span::before { - width: 18px; - } - - .modelStatsLayout { - grid-template-columns: minmax(0, 1fr) minmax(160px, 190px); - gap: 12px; - } - - .modelStatsCard .modelSharePanel { - grid-template-columns: 1fr; - justify-items: center; - gap: 8px; - } - - .modelStatsCard .modelShareHeader, - .modelStatsCard .modelLegend, - .modelStatsCard .donutChart { - grid-column: auto; - grid-row: auto; - } - - .modelStatsCard .donutChart { - width: min(140px, 100%); - } - - .modelStatsCard .modelLegend { - gap: 4px; - } - - .modelSharePanel .donutTooltip { - right: auto; - bottom: auto; - left: 50%; - top: calc(100% + 12px); - transform: translate(-50%, 0); - } - - .modelSharePanel .donutChart:hover .donutTooltip, - .modelSharePanel .donutChart:focus-within .donutTooltip { - transform: translate(-50%, 0); - } -} - -@container monitoring-page (max-width: 768px) { - .masthead, - .panel { - border-radius: 12px; - } - - .panel { - padding: 16px; - } - - .masthead { - gap: 16px; - } - - .titleRow { - grid-template-columns: 1fr; - } - - .titleActions { - justify-content: flex-start; - width: 100%; - } - - .title { - font-size: 28px; - } - - .subtitle { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; - font-size: 12px; - line-height: 1.5; - } - - .mastheadActionButton { - flex: 0 0 auto; - } - - .toolbarControlRow { - width: 100%; - flex-direction: column; - align-items: stretch; - } - - .segmentedControl, - .refreshCluster { - width: 100%; - max-width: 100%; - min-width: 0; - justify-content: flex-start; - } - - .refreshCluster { - flex-wrap: wrap; - } - - .refreshControls { - min-width: 0; - flex: 0 1 auto; - } - - .segmentedControl { - flex: 0 0 auto; - } - - .quickLinkButton { - flex: 0 0 auto; - } - - .realtimePanel .panelHeader { - flex-direction: column; - align-items: stretch; - gap: 10px; - } - - .realtimePanel .panelTitle { - flex: 0 1 auto; - min-width: 0; - overflow: hidden; - font-size: 16px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .realtimePanel .panelExtra { - flex: 1 1 auto; - min-width: 0; - width: 100%; - } - - .toolbarHeaderActions { - width: 100%; - } - - .toolbarHeaderActions .clearButton { - flex: 0 0 auto; - } - - .autoRefreshField { - flex: 1 1 160px; - min-width: 0; - width: auto; - justify-content: flex-start; - } - - .autoRefreshSelect { - width: 76px; - } - - .toolbarHeaderActions { - flex-wrap: nowrap; - } - - .toolbarHeaderSearchInput { - min-width: 0; - } - - .refreshCluster { - overflow-x: visible; - scrollbar-width: none; - } - - .refreshCluster::-webkit-scrollbar { - display: none; - } - - .syncPill { - min-width: 0; - max-width: 112px; - overflow: hidden; - padding: 0 8px; - font-size: 11px; - text-overflow: ellipsis; - } - - .autoRefreshLabel { - font-size: 10px; - } - - .autoRefreshLabel svg { - display: none; - } - - .autoRefreshSelectTrigger { - min-width: 68px; - } - - .refreshButtonLabel { - display: none; - } - - .refreshButton { - min-width: 42px; - } - - .usageStatsCard { - padding: 13px; - } - - .usageStatsBody strong { - font-size: 22px; - } - - .usageStatsFooter { - gap: 6px; - } - - .filterGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .rankingGrid { - grid-template-columns: 1fr; - } - - .filterGrid :global(.select-trigger) { - padding-inline: 8px !important; - } - - .filterGrid :global(.select-value) { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .settingsGrid { - grid-template-columns: 1fr; - } - - .settingsDangerAction { - align-items: stretch; - flex-direction: column; - } - - .settingsDangerAction :global(.btn) { - width: 100%; - } - - .segmentButton { - min-height: 32px; - padding: 0 10px; - font-size: 12px; - } - - .refreshCluster { - gap: 8px; - padding: 6px; - } - - .refreshControls { - flex: 1 1 auto; - justify-content: flex-end; - } - - .autoRefreshField { - flex-basis: 132px; - } - - .syncPill, - .autoRefreshField, - .refreshButton { - min-height: 38px; - } - - .refreshButton { - min-width: 42px; - padding: 0 10px; - } - - .filterGrid { - gap: 8px; - } - - .priceActionsBar { - justify-content: stretch; - } - - .priceActionsBar :global(.btn) { - flex: 1 1 0; - } - - .table { - min-width: 0; - } - - .accountOverviewTable { - min-width: 920px; - } - - .realtimeTable { - min-width: 1360px; - } -} - -@media (max-width: 900px) { - .priceManagerModal :global(.modal-body) { - max-height: min(76vh, 680px); - } - - .priceRuleWorkspace { - grid-template-columns: 1fr; - grid-template-rows: 154px minmax(0, 1fr); - height: min(70vh, 620px); - } - - .priceRuleSidebar { - grid-template-rows: auto minmax(0, 1fr); - border-right: 0; - border-bottom: 1px solid var(--monitor-line); - } - - .priceRuleSearch { - padding: 10px 14px; - } - - .priceRuleList { - display: flex; - gap: 7px; - overflow-x: auto; - overflow-y: hidden; - padding: 7px 10px 10px; - } - - .priceRuleListItem { - flex: 0 0 220px; - min-height: 58px; - padding: 8px 10px; - } - - .priceRuleListEmpty { - flex: 1 1 100%; - min-height: 58px; - } - - .priceSyncHeader { - align-items: flex-start; - flex-direction: column; - } - - .priceSyncActions { - width: 100%; - flex-wrap: wrap; - } - - .priceSyncSchedule { - align-items: stretch; - flex-direction: column; - } - - .priceSyncScheduleControls { - width: 100%; - flex-wrap: wrap; - } - - .priceSyncChangesHeader { - align-items: flex-start; - flex-direction: column; - } - - .priceSyncChangesToolbar { - width: 100%; - justify-content: space-between; - } - - .priceSyncChangeFilters { - flex: 1 1 100%; - width: 100%; - max-width: 100%; - overflow-x: auto; - } - - .priceSyncChangeRow { - grid-template-columns: 1fr; - gap: 8px; - } - - .priceSyncRateChanges { - justify-content: flex-start; - } -} - -@media (max-width: 680px) { - .priceManagerModal :global(.modal-body) { - max-height: min(72vh, calc(100dvh - 150px)); - } - - .priceManagerTabs { - padding-inline: 10px 46px; - } - - .priceManagerTab { - min-width: 0; - flex: 1 1 0; - justify-content: center; - padding-inline: 8px; - } - - .priceRuleWorkspace { - grid-template-rows: 150px minmax(0, 1fr); - height: min(66vh, calc(100dvh - 160px)); - } - - .priceRuleEditorHeader, - .priceRuleSection, - .priceRuleEditorFooter, - .priceSyncView { - padding-inline: 14px; - } - - .priceRuleEditorHeader { - min-height: 66px; - } - - .priceRuleEditorBadges > span:not(:first-child) { - display: none; - } - - .priceBaseGrid { - grid-template-columns: 1fr; - gap: 10px; - } - - .priceTierCompactRow { - position: relative; - grid-template-columns: repeat(2, minmax(0, 1fr)); - padding: 34px 10px 10px; - } - - .priceTierCompactRow label:first-of-type { - grid-column: 1 / -1; - } - - .priceTierIndex { - position: absolute; - top: 8px; - left: 10px; - width: 24px; - height: 20px; - } - - .priceTierRemoveButton { - position: absolute; - top: 6px; - right: 8px; - width: 30px; - height: 26px; - } - - .priceRuleEditorFooter { - align-items: stretch; - flex-direction: column-reverse; - } - - .priceRuleEditorFooter > div { - width: 100%; - } - - .priceRuleEditorFooter > div:last-child :global(.btn) { - flex: 1 1 0; - } - - .priceSyncView { - padding-block: 18px; - } - - .priceSyncActions :global(.btn) { - flex: 1 1 132px; - } - - .priceSyncMetrics { - grid-template-columns: repeat(2, minmax(0, 1fr)); - min-height: 158px; - } - - .priceSyncMetrics > div:nth-child(2) { - border-right: 0; - } - - .priceSyncMetrics > div:nth-child(n + 3) { - border-top: 1px solid var(--monitor-line); - } - - .priceSyncOverrideAll { - width: 100%; - justify-content: center; - } - - .priceSyncChangeFilters button { - flex: 0 0 auto; - } - - .priceSyncScheduleControls { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: end; - } - - .priceSyncScheduleToggle { - grid-column: 1 / -1; - } - - .priceSyncScheduleInterval { - width: 100%; - } - - .priceSyncSchedule :global(.btn) { - min-height: 34px; - } - - .priceUnmatchedList > div:not(.priceTierEmpty) { - align-items: flex-start; - } -} diff --git a/cliproxyapi-pro-management/overlay/src/pages/MonitoringCenterPage.tsx b/cliproxyapi-pro-management/overlay/src/pages/MonitoringCenterPage.tsx index cfe1ae6..cbdd72a 100644 --- a/cliproxyapi-pro-management/overlay/src/pages/MonitoringCenterPage.tsx +++ b/cliproxyapi-pro-management/overlay/src/pages/MonitoringCenterPage.tsx @@ -1,29 +1,20 @@ import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type DragEvent, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; -import type { TFunction } from 'i18next'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { Input } from '@/components/ui/Input'; import { Modal } from '@/components/ui/Modal'; import { Select } from '@/components/ui/Select'; import { - IconChevronDown, - IconChevronUp, - IconRefreshCw, IconSearch, IconSlidersHorizontal, - IconTrash2, } from '@/components/ui/icons'; import { buildAccountRowsByAccount, buildLocalDayKey, - formatShortDateTime, getRangeStartMs, - joinUnique, useMonitoringData, - type MonitoringAccountRow, type MonitoringEventRow, - type MonitoringStatusTone, type MonitoringTimeRange, } from '@/features/monitoring/hooks/useMonitoringData'; import { REALTIME_LOG_PAGE_SIZE, useRealtimeLogData } from '@/features/monitoring/hooks/useRealtimeLogData'; @@ -31,11 +22,23 @@ import { useUsageData, type UsageEventPageFilters, type UsagePayload } from '@/f import { useUsageAggregates, type UsageAggregateBucket } from '@/features/monitoring/hooks/useUsageAggregates'; import { findMonitoringAuthIndexes } from '@/features/monitoring/monitoringAuthSearch'; import { - buildAccountStatusData, - buildAccountStatusRange, - type AccountStatusData, -} from '@/features/monitoring/accountHealth'; -import { MonitoringHealthStatusBar } from '@/features/monitoring/components/MonitoringHealthStatusBar'; + buildAccountQuotaEntriesByAccount, + buildAccountQuotaTargetsByAccount, + getQuotaForTarget, + requestAccountQuota, + settleWithConcurrency, + type AccountQuotaSourceRow, + type AccountQuotaState, + type AnyQuotaConfig, +} from '@/features/monitoring/accountQuota'; +import { AccountStatsPanel } from '@/features/monitoring/components/AccountStatsPanel'; +import { ModelPriceManagerModal } from '@/features/monitoring/components/ModelPriceManagerModal'; +import { MonitoringSettingsModal } from '@/features/monitoring/components/MonitoringSettingsModal'; +import { + RealtimeErrorDetailsPanel, + RecentPattern, + StatusBadge, +} from '@/features/monitoring/components/RealtimeLogDetails'; import { RealtimeCostCell, } from '@/features/monitoring/components/RealtimeCostCell'; @@ -60,17 +63,29 @@ import { createMonitoringSummaryAccumulator, finalizeMonitoringSummary, formatPercent, - getAccountHealthTone, getAccountSortValue, getRankingMetricValue, - type AccountHealthTone, type AccountSortMetric, type RankingMetric, } from '@/features/monitoring/monitoringAnalytics'; +import { TIME_RANGE_OPTIONS } from '@/features/monitoring/monitoringOptions'; +import { + buildMonitoringSettingsFromDraft, + createMonitoringSettingsDraft, + type MonitoringSettings, + type MonitoringSettingsDraft, +} from '@/features/monitoring/monitoringSettings'; import { - ACCOUNT_SORT_OPTIONS, - TIME_RANGE_OPTIONS, -} from '@/features/monitoring/monitoringOptions'; + createPriceDraft, + formatDeltaPercent, + parsePriceContextSize, + parsePriceValue, + type PriceDraft, + type PriceManagementView, + type PriceRuleTarget, + type PriceSyncChangeFilter, + type PriceTierDraft, +} from '@/features/monitoring/modelPricePresentation'; import { REALTIME_LOG_COLUMN_DEFAULT_WIDTHS, clampRealtimeLogColumnWidth, @@ -89,9 +104,7 @@ import { buildRealtimeLogPageRows, buildRealtimeMetaText, buildRealtimeStatusLabel, - compactRealtimeErrorMessage, getClientPaginationRange, - translateRealtimeErrorCategory, translateRealtimeErrorText, type RealtimeLogRow, } from '@/features/monitoring/realtimeLogPresentation'; @@ -101,7 +114,7 @@ import { apiClient } from '@/services/api/client'; import { useAuthStore, useConfigStore, useNotificationStore, useQuotaStore } from '@/stores'; import type { AuthFileItem } from '@/types'; import { maskSensitiveText } from '@/utils/format'; -import { getStatusFromError, isAntigravityFile, isClaudeFile, isCodexFile, isKimiFile, isXaiFile } from '@/utils/quota'; +import { getStatusFromError } from '@/utils/quota'; import { deleteModelPriceRule, formatCompactNumber, @@ -115,26 +128,14 @@ import { saveModelPriceRule, syncModelPricesFromModelsDev, type ModelPriceRule, - type ModelPriceSyncChangeAction, type ModelPriceSyncResult, type ModelPriceSyncState, type ObservedModelPriceTarget, } from '@/utils/usage'; -import { - ANTIGRAVITY_CONFIG, - CLAUDE_CONFIG, - CODEX_CONFIG, - KIMI_CONFIG, - XAI_CONFIG, - type QuotaConfig, - type QuotaStore, -} from '@/components/quota/quotaConfigs'; -import { type QuotaRenderHelpers, type QuotaStatusState } from '@/components/quota/QuotaCard'; -import { QuotaProgressBar as AuthFileQuotaProgressBar } from '@/features/authFiles/components/QuotaProgressBar'; -import authFileQuotaStyles from '@/pages/AuthFilesPage.module.scss'; +import type { QuotaStatusState } from '@/components/quota/QuotaCard'; import quotaStyles from '@/pages/QuotaPage.module.scss'; import { quotaPersistenceMiddleware } from '@/extensions/quota/persistenceMiddleware'; -import styles from './MonitoringCenterPage.module.scss'; +import styles from '@/features/monitoring/monitoring.module.scss'; type StatusFilter = 'all' | 'success' | 'failed'; @@ -155,6 +156,10 @@ const getCacheHitRate = (row: Pick 0 ? Math.min(Math.max(row.cachedTokens / row.inputTokens, 0), 1) : null ); +const getSuccessRateClassName = (rate: number) => ( + rate >= 0.95 ? styles.goodText : rate >= 0.85 ? styles.warnText : styles.badText +); + const getRealtimeLogColumnContentTexts = (key: RealtimeLogColumnKey, row: RealtimeLogRow) => { switch (key) { case 'type': @@ -221,63 +226,6 @@ const estimateRealtimeLogHeaderWidth = (key: RealtimeLogColumnKey, label: string return clampRealtimeLogColumnWidth(key, textWidth + 42); }; -const formatAccountOverviewScopeText = (rangeLabel: string, t: TFunction) => t('monitoring.account_scope_text', { range: rangeLabel }); - -type PriceTierDraft = { - contextSize: string; - input: string; - output: string; - cacheRead: string; - cacheWrite: string; -}; - -type PriceDraft = { - input: string; - output: string; - cacheRead: string; - cacheWrite: string; - tiers: PriceTierDraft[]; -}; - -type PriceManagementView = 'rules' | 'sync'; -type PriceSyncChangeFilter = 'all' | ModelPriceSyncChangeAction; - -type PriceRuleTarget = { - key: string; - model: string; - requests: number; - lastSeenAtMs: number; - rule?: ModelPriceRule; -}; - -type MonitoringSettings = { - retentionDays: number; - webdav: { - enabled: boolean; - intervalMinutes: number; - retentionDays: number; - url: string; - username: string; - password: string; - }; - modelPriceSync: { - enabled: boolean; - intervalMinutes: number; - }; -}; - -type MonitoringSettingsDraft = { - retentionDays: string; - webdavEnabled: boolean; - webdavIntervalMinutes: string; - webdavRetentionDays: string; - webdavUrl: string; - webdavUsername: string; - webdavPassword: string; - modelPriceSyncEnabled: boolean; - modelPriceSyncIntervalMinutes: string; -}; - type UsageImportResult = { added?: number; skipped?: number; @@ -307,1093 +255,6 @@ type UsageResetResult = { resetAtMs: number; }; -const createMonitoringSettingsDraft = (settings?: MonitoringSettings): MonitoringSettingsDraft => ({ - retentionDays: String(settings?.retentionDays ?? 0), - webdavEnabled: settings?.webdav.enabled ?? false, - webdavIntervalMinutes: String(settings?.webdav.intervalMinutes ?? 1440), - webdavRetentionDays: String(settings?.webdav.retentionDays ?? 0), - webdavUrl: settings?.webdav.url ?? '', - webdavUsername: settings?.webdav.username ?? '', - webdavPassword: settings?.webdav.password ?? '', - modelPriceSyncEnabled: settings?.modelPriceSync?.enabled ?? false, - modelPriceSyncIntervalMinutes: String(settings?.modelPriceSync?.intervalMinutes ?? 1440), -}); - -const parseNonNegativeInteger = (value: string) => { - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0; -}; - -const parsePositiveInteger = (value: string, fallback: number) => { - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; -}; - -const buildMonitoringSettingsFromDraft = (draft: MonitoringSettingsDraft): MonitoringSettings => ({ - retentionDays: parseNonNegativeInteger(draft.retentionDays), - webdav: { - enabled: draft.webdavEnabled, - intervalMinutes: parsePositiveInteger(draft.webdavIntervalMinutes, 1440), - retentionDays: parseNonNegativeInteger(draft.webdavRetentionDays), - url: draft.webdavUrl.trim(), - username: draft.webdavUsername.trim(), - password: draft.webdavPassword, - }, - modelPriceSync: { - enabled: draft.modelPriceSyncEnabled, - intervalMinutes: parsePositiveInteger(draft.modelPriceSyncIntervalMinutes, 1440), - }, -}); - - -const SuccessFailureValue = ({ success, failure }: { success: number; failure: number }) => ( - - {formatCompactNumber(success)} - ({formatCompactNumber(failure)}) - -); - -const buildAccountCardFileName = (row: MonitoringAccountRow, quotaEntries: AccountQuotaEntry[] = []) => { - const quotaFileNames = Array.from(new Set(quotaEntries.map((entry) => entry.fileName).filter(Boolean))); - if (quotaFileNames.length > 0) return joinUnique(quotaFileNames, 1); - - const fileName = row.authLabels.find((label) => label && label !== '-' && label.endsWith('.json')); - return fileName || row.authLabels.find((label) => label && label !== '-') || row.accountMasked || row.account; -}; - -const buildAccountCardProviderText = (row: MonitoringAccountRow) => { - const providers = row.providers.filter((provider) => provider && provider !== '-'); - return providers.length > 0 ? joinUnique(providers, 2) : '-'; -}; - -const sortAccountOverviewCardMetrics = (metrics: AccountSummaryMetric[], t: TFunction) => { - const labels: Record = { - 'total-tokens': t('monitoring.token_metric_total'), - 'input-tokens': t('monitoring.token_metric_input'), - 'output-tokens': t('monitoring.token_metric_output'), - 'cached-tokens': t('monitoring.token_metric_cached'), - }; - const order = ['total-tokens', 'input-tokens', 'output-tokens', 'cached-tokens']; - return order - .map((key) => { - const metric = metrics.find((item) => item.key === key); - return metric ? { ...metric, label: labels[key] } : undefined; - }) - .filter(Boolean) as AccountSummaryMetric[]; -}; - -const buildAccountSummaryMetrics = ( - row: MonitoringAccountRow, - hasPrices: boolean, - locale: string, - t: TFunction -): AccountSummaryMetric[] => [ - { - key: 'total-calls', - label: t('monitoring.total_calls'), - value: formatCompactNumber(row.totalCalls), - }, - { - key: 'success-calls', - label: t('monitoring.success_calls'), - value: , - }, - { - key: 'success-rate', - label: t('monitoring.call_success_rate'), - value: formatPercent(row.successRate), - valueClassName: - row.successRate >= 0.95 - ? styles.goodText - : row.successRate >= 0.85 - ? styles.warnText - : styles.badText, - }, - { - key: 'total-tokens', - label: t('monitoring.total_tokens'), - value: formatCompactNumber(row.totalTokens), - }, - { - key: 'input-tokens', - label: t('monitoring.input_tokens'), - value: formatCompactNumber(row.inputTokens), - }, - { - key: 'output-tokens', - label: t('monitoring.output_tokens'), - value: formatCompactNumber(row.outputTokens), - }, - { - key: 'cached-tokens', - label: t('monitoring.cached_tokens'), - value: formatCompactNumber(row.cachedTokens), - }, - { - key: 'estimated-cost', - label: t('monitoring.estimated_cost'), - value: hasPrices ? formatUsd(row.totalCost) : '--', - }, - { - key: 'latest-request-time', - label: t('monitoring.latest_request_time'), - value: new Date(row.lastSeenAt).toLocaleString(locale), - }, -]; - -type AnyQuotaConfig = { - type: QuotaConfig['type']; - i18nPrefix: string; - fetchQuota: (file: AuthFileItem, t: TFunction) => Promise; - storeSelector: (state: QuotaStore) => Record; - storeSetter: keyof QuotaStore; - buildLoadingState: () => QuotaStatusState; - buildSuccessState: (data: unknown) => QuotaStatusState; - buildErrorState: (message: string, status?: number) => QuotaStatusState; - renderQuotaItems: (quota: QuotaStatusState, t: TFunction, helpers: QuotaRenderHelpers) => ReactNode; -}; - -const adaptQuotaConfig = ( - config: QuotaConfig -): AnyQuotaConfig => ({ - type: config.type, - i18nPrefix: config.i18nPrefix, - fetchQuota: config.fetchQuota, - storeSelector: config.storeSelector, - storeSetter: config.storeSetter, - buildLoadingState: config.buildLoadingState, - buildSuccessState: (data) => config.buildSuccessState(data as TData), - buildErrorState: config.buildErrorState, - renderQuotaItems: (quota, t, helpers) => config.renderQuotaItems(quota as TState, t, helpers), -}); - -const ACCOUNT_ANTIGRAVITY_QUOTA_CONFIG = adaptQuotaConfig(ANTIGRAVITY_CONFIG); -const ACCOUNT_CLAUDE_QUOTA_CONFIG = adaptQuotaConfig(CLAUDE_CONFIG); -const ACCOUNT_CODEX_QUOTA_CONFIG = adaptQuotaConfig(CODEX_CONFIG); -const ACCOUNT_KIMI_QUOTA_CONFIG = adaptQuotaConfig(KIMI_CONFIG); -const ACCOUNT_XAI_QUOTA_CONFIG = adaptQuotaConfig(XAI_CONFIG); - -type AccountQuotaTarget = { - key: string; - authIndex: string; - authLabel: string; - fileName: string; - file: AuthFileItem; - config: AnyQuotaConfig; -}; - -type AccountQuotaEntry = { - key: string; - authLabel: string; - fileName: string; - providerLabel: string; - quota?: QuotaStatusState; - config: AnyQuotaConfig; -}; - -type AccountQuotaState = { - status: 'idle' | 'loading' | 'success' | 'error'; - targetKey: string; - error?: string; - lastRefreshedAt?: number; -}; - -type AccountQuotaSourceRow = Pick; - -type AccountSummaryMetric = { - key: string; - label: string; - value: ReactNode; - valueClassName?: string; -}; - -const roundCurrency = (value: number) => Math.round(value * 100) / 100; - -const formatDeltaPercent = (current: number, previous: number) => { - const roundedCurrent = roundCurrency(current); - const roundedPrevious = roundCurrency(previous); - if (roundedPrevious <= 0) return roundedCurrent > 0 ? '+100.0%' : '0.0%'; - const delta = (roundedCurrent - roundedPrevious) / roundedPrevious; - return `${delta >= 0 ? '+' : ''}${(delta * 100).toFixed(1)}%`; -}; - -const createPriceDraft = (rule?: ModelPriceRule): PriceDraft => ({ - input: rule ? String(rule.base.input) : '', - output: rule ? String(rule.base.output) : '', - cacheRead: rule ? String(rule.base.cacheRead) : '', - cacheWrite: rule ? String(rule.base.cacheWrite) : '', - tiers: rule?.tiers?.map((tier) => ({ - contextSize: String(tier.contextSize), - input: String(tier.input), - output: String(tier.output), - cacheRead: String(tier.cacheRead), - cacheWrite: String(tier.cacheWrite), - })) ?? [], -}); - -const parsePriceValue = (value: string) => { - const parsed = Number.parseFloat(value); - return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0; -}; - -const formatModelPriceRate = (value: number | undefined) => { - const normalized = Number(value) || 0; - return `$${normalized.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}`; -}; - -const MODEL_PRICE_SYNC_RATE_FIELDS = [ - ['input', 'usage_stats.model_price_input'], - ['output', 'usage_stats.model_price_output'], - ['cacheRead', 'usage_stats.model_price_cache_read'], - ['cacheWrite', 'usage_stats.model_price_cache_write'], -] as const; - -const ACCOUNT_QUOTA_RENDER_HELPERS: QuotaRenderHelpers = { - styles: { - ...authFileQuotaStyles, - quotaRow: `${authFileQuotaStyles.quotaRow} ${styles.accountQuotaRow}`, - quotaRowHeader: `${authFileQuotaStyles.quotaRowHeader} ${styles.accountQuotaRowHeader}`, - quotaModel: `${authFileQuotaStyles.quotaModel} ${styles.accountQuotaModel}`, - quotaMeta: `${authFileQuotaStyles.quotaMeta} ${styles.accountQuotaMeta}`, - quotaAmount: `${authFileQuotaStyles.quotaAmount} ${styles.accountQuotaAmount}`, - codexPlanValue: `${authFileQuotaStyles.codexPlanValue} ${styles.accountQuotaPlanValue}`, - premiumPlanValue: `${authFileQuotaStyles.premiumPlanValue} ${styles.accountQuotaPremiumPlanValue}`, - codexResetCreditRow: `${authFileQuotaStyles.codexResetCreditRow} ${styles.accountQuotaResetCreditRow}`, - codexResetCreditTime: `${authFileQuotaStyles.codexResetCreditTime} ${styles.accountQuotaResetCreditTime}`, - }, - QuotaProgressBar: AuthFileQuotaProgressBar, -}; - -const getQuotaProviderLabel = (config: AnyQuotaConfig, t: TFunction) => { - const titleKey = `${config.i18nPrefix}.title`; - const translated = t(titleKey); - if (translated !== titleKey) return translated; - return config.type; -}; - -const getAccountQuotaConfig = (file: AuthFileItem): AnyQuotaConfig | undefined => { - if (isAntigravityFile(file)) return ACCOUNT_ANTIGRAVITY_QUOTA_CONFIG; - if (isClaudeFile(file)) return ACCOUNT_CLAUDE_QUOTA_CONFIG; - if (isCodexFile(file)) return ACCOUNT_CODEX_QUOTA_CONFIG; - if (isKimiFile(file)) return ACCOUNT_KIMI_QUOTA_CONFIG; - if (isXaiFile(file)) return ACCOUNT_XAI_QUOTA_CONFIG; - return undefined; -}; - -const resolveQuotaErrorMessage = (t: TFunction, quota?: QuotaStatusState): string => { - if (!quota) return t('common.unknown_error'); - if (quota.errorStatus === 404) return t('common.quota_update_required'); - if (quota.errorStatus === 403) return t('common.quota_check_credential'); - return quota.error || t('common.unknown_error'); -}; - -const hasUsableQuotaContent = (quota?: QuotaStatusState) => { - if (!quota || quota.status !== 'success') return false; - const record = quota as unknown as Record; - const billing = record.billing; - return ['groups', 'windows', 'buckets', 'rows'].some((key) => { - const value = record[key]; - return Array.isArray(value) && value.length > 0; - }) || Boolean( - record.planType - || record.tierLabel - || record.creditBalance !== undefined - || (billing && typeof billing === 'object' && !Array.isArray(billing)) - ); -}; - -const getQuotaForTarget = (store: QuotaStore, target: AccountQuotaTarget): QuotaStatusState | undefined => { - return target.config.storeSelector(store)[target.fileName] as QuotaStatusState | undefined; -}; - -const requestAccountQuota = async ( - target: AccountQuotaTarget, - t: TFunction -): Promise => { - try { - const data = await target.config.fetchQuota(target.file, t); - return target.config.buildSuccessState(data) as QuotaStatusState; - } catch (err: unknown) { - const message = err instanceof Error ? err.message : t('common.unknown_error'); - return target.config.buildErrorState(message, getStatusFromError(err)) as QuotaStatusState; - } -}; - -const settleWithConcurrency = async ( - items: T[], - concurrency: number, - worker: (item: T) => Promise -): Promise>> => { - const results = new Array>(items.length); - let nextIndex = 0; - const workerCount = Math.min(Math.max(concurrency, 1), items.length); - await Promise.all(Array.from({ length: workerCount }, async () => { - while (nextIndex < items.length) { - const index = nextIndex; - nextIndex += 1; - try { - results[index] = { status: 'fulfilled', value: await worker(items[index]) }; - } catch (reason) { - results[index] = { status: 'rejected', reason }; - } - } - })); - return results; -}; - -const buildAccountQuotaTargetsByAccount = ( - rows: AccountQuotaSourceRow[], - authFilesByAuthIndex: Map -) => { - const grouped = new Map>(); - - rows.forEach((row) => { - const authIndex = normalizeAuthIndex(row.authIndex); - if (!authIndex || !row.account) return; - - const file = authFilesByAuthIndex.get(authIndex); - if (!file) return; - - const quotaConfig = getAccountQuotaConfig(file); - if (!quotaConfig) return; - - const dedupeKey = `${quotaConfig.type}::${authIndex}::${file.name}`; - const bucket = grouped.get(row.account) ?? new Map(); - if (!bucket.has(dedupeKey)) { - bucket.set(dedupeKey, { - key: dedupeKey, - authIndex, - authLabel: row.authLabel || file.name || authIndex, - fileName: file.name, - file, - config: quotaConfig, - }); - } - grouped.set(row.account, bucket); - }); - - return new Map( - Array.from(grouped.entries()).map(([account, bucket]) => [ - account, - Array.from(bucket.values()).sort((left, right) => left.authLabel.localeCompare(right.authLabel)), - ]) - ); -}; - -const buildAccountQuotaEntriesByAccount = ( - targetsByAccount: Map, - quotaStore: QuotaStore, - t: TFunction -) => new Map( - Array.from(targetsByAccount.entries()).map(([account, targets]) => [ - account, - targets.map((target) => ({ - key: target.key, - authLabel: target.authLabel, - fileName: target.fileName, - providerLabel: getQuotaProviderLabel(target.config, t), - quota: getQuotaForTarget(quotaStore, target), - config: target.config, - } satisfies AccountQuotaEntry)), - ]) -); - -const getSuccessRateClassName = (rate: number) => - rate >= 0.95 ? styles.goodText : rate >= 0.85 ? styles.warnText : styles.badText; - -const getAccountStatusDotClassName = (tone: AccountHealthTone) => { - if (tone === 'good') return styles.accountStatusDotEnabled; - if (tone === 'warn') return styles.accountStatusDotMixed; - return styles.accountStatusDotDisabled; -}; - -function AccountHealthStatusPanel({ - row, - hasPrices, - locale, - t, - statusData, - scopeText, -}: { - row: MonitoringAccountRow; - hasPrices: boolean; - locale: string; - t: TFunction; - statusData: AccountStatusData; - scopeText: string; -}) { - const healthMetrics = [ - { key: 'total-calls', label: t('monitoring.total_calls'), value: formatCompactNumber(row.totalCalls) }, - { - key: 'success-failure', - label: t('monitoring.success_failure'), - value: , - }, - { key: 'estimated-cost', label: t('monitoring.estimated_cost'), value: hasPrices ? formatUsd(row.totalCost) : '--', className: styles.primaryText }, - { key: 'success-rate', label: t('monitoring.success_rate'), value: formatPercent(row.successRate), className: getSuccessRateClassName(row.successRate) }, - ]; - - return ( -
-
- {t('monitoring.account_health_status')} - - i - -
-
- {healthMetrics.map((metric) => ( -
- {metric.label} - {metric.value} -
- ))} -
- -
{scopeText}
-
- ); -} - -function AccountTokenMetricGrid({ metrics, t }: { metrics: AccountSummaryMetric[]; t: TFunction }) { - const getTokenMetricToneClassName = (key: string) => { - if (key === 'input-tokens') return styles.accountMetricIconInput; - if (key === 'output-tokens') return styles.accountMetricIconOutput; - if (key === 'cached-tokens') return styles.accountMetricIconCached; - return styles.accountMetricIconTotal; - }; - - return ( -
-
- {t('monitoring.token_usage')} -
-
- {metrics.map((metric) => ( -
- - - - {metric.value} - -
- ))} -
-
- ); -} - -function AccountModelUsageList({ - row, - hasPrices, - locale, - t, - limit = 1, -}: { - row: MonitoringAccountRow; - hasPrices: boolean; - locale: string; - t: TFunction; - limit?: number; -}) { - const [showAll, setShowAll] = useState(false); - const [expandedModels, setExpandedModels] = useState>({}); - const hasExtraModels = row.models.length > limit; - const visibleModels = showAll ? row.models : row.models.slice(0, limit); - const toggleModel = (key: string) => setExpandedModels((previous) => ({ ...previous, [key]: !previous[key] })); - - return ( -
-
- {t('monitoring.top_models')} - {hasExtraModels ? ( - - ) : null} -
- - {visibleModels.length > 0 ? ( -
- {visibleModels.map((model) => { - const modelKey = `${row.id}-${model.model}`; - const isModelExpanded = Boolean(expandedModels[modelKey]); - return ( -
- - {isModelExpanded ? ( -
-
{t('monitoring.input_tokens')}{formatCompactNumber(model.inputTokens)}
-
{t('monitoring.output_tokens')}{formatCompactNumber(model.outputTokens)}
-
{t('monitoring.cached_tokens')}{formatCompactNumber(model.cachedTokens)}
-
{t('monitoring.latest_request_time')}{new Date(model.lastSeenAt).toLocaleString(locale)}
-
- ) : null} -
- ); - })} -
- ) : ( -
{t('monitoring.no_model_data')}
- )} -
- ); -} - -function AccountQuotaPanel({ - quotaState, - quotaEntries, - locale, - t, - onRefreshQuota, -}: { - quotaState?: AccountQuotaState; - quotaEntries: AccountQuotaEntry[]; - locale: string; - t: TFunction; - onRefreshQuota: () => void; -}) { - const quotaLoading = quotaState?.status === 'loading'; - const lastQuotaSync = quotaState?.lastRefreshedAt && Number.isFinite(quotaState.lastRefreshedAt) - ? new Date(quotaState.lastRefreshedAt).toLocaleString(locale) - : ''; - const quotaTitle = quotaEntries.length === 1 ? quotaEntries[0].providerLabel : t('quota_management.title'); - - const renderRefreshButton = () => ( - - ); - - return ( -
-
-
- {quotaTitle} - {lastQuotaSync ? {`${t('monitoring.last_sync')}: ${lastQuotaSync}`} : null} -
- {renderRefreshButton()} -
- - {quotaLoading && quotaEntries.length === 0 ?
{t('codex_quota.loading')}
: null} - {!quotaLoading && quotaState?.status === 'error' && quotaEntries.length === 0 ? ( -
{t('codex_quota.load_failed', { message: quotaState.error || t('common.unknown_error') })}
- ) : null} - {!quotaLoading && quotaState?.status === 'success' && quotaEntries.length === 0 ?
{t('monitoring.account_quota_empty')}
: null} - {!quotaState && quotaEntries.length === 0 ?
{t('monitoring.account_quota_empty')}
: null} - - {quotaEntries.length > 0 ? ( -
- {quotaEntries.map((entry) => ( -
-
-
- {entry.authLabel} -
-
- - {entry.quota?.status === 'loading' ? ( -
{t(`${entry.config.i18nPrefix}.loading`)}
- ) : entry.quota?.status === 'error' ? ( -
- {t(`${entry.config.i18nPrefix}.load_failed`, { message: resolveQuotaErrorMessage(t, entry.quota) })} -
- ) : hasUsableQuotaContent(entry.quota) ? ( -
- {entry.config.renderQuotaItems(entry.quota!, t, ACCOUNT_QUOTA_RENDER_HELPERS)} -
- ) : ( -
{t(`${entry.config.i18nPrefix}.idle`)}
- )} -
- ))} -
- ) : null} -
- ); -} - -function AccountExpandedDetails({ - row, - hasPrices, - locale, - t, - quotaState, - quotaEntries, - onRefreshQuota, -}: { - row: MonitoringAccountRow; - hasPrices: boolean; - locale: string; - t: TFunction; - quotaState?: AccountQuotaState; - quotaEntries: AccountQuotaEntry[]; - onRefreshQuota: () => void; -}) { - return ( -
- - -
- ); -} - -function AccountOverviewCard({ - row, - hasPrices, - locale, - t, - isExpanded, - statusData, - scopeText, - quotaState, - quotaEntries, - onToggle, - onRefreshQuota, -}: { - row: MonitoringAccountRow; - hasPrices: boolean; - locale: string; - t: TFunction; - isExpanded: boolean; - statusData: AccountStatusData; - scopeText: string; - quotaState?: AccountQuotaState; - quotaEntries: AccountQuotaEntry[]; - onToggle: () => void; - onRefreshQuota: () => void; -}) { - const summaryMetrics = buildAccountSummaryMetrics(row, hasPrices, locale, t); - const cardMetrics = sortAccountOverviewCardMetrics(summaryMetrics, t); - const tone = getAccountHealthTone(row); - const latestRequestText = new Date(row.lastSeenAt).toLocaleString(locale); - const accountLabel = buildAccountCardFileName(row, quotaEntries); - const providerText = buildAccountCardProviderText(row); - - return ( - -
-
- - - {tone === 'good' ? t('monitoring.health_good') : tone === 'warn' ? t('monitoring.health_warn') : t('monitoring.health_bad')} - -
-
- {providerText} - · - {t('monitoring.latest_request_time_value', { value: latestRequestText })} -
-
- - - - - {isExpanded ? ( - - ) : null} -
- ); -} - -function AccountStatsPanel({ - rows, - metric, - emptyText, - hasPrices, - locale, - t, - rangeLabel, - range, - onRangeChange, - onHide, - expandedAccounts, - accountQuotaStates, - accountQuotaEntriesByAccount, - onMetricChange, - onToggleAccount, - onRefreshQuota, -}: { - rows: MonitoringAccountRow[]; - metric: AccountSortMetric; - emptyText: string; - hasPrices: boolean; - locale: string; - t: TFunction; - rangeLabel: string; - range: MonitoringTimeRange; - onRangeChange: (range: MonitoringTimeRange) => void; - onHide: () => void; - expandedAccounts: Record; - accountQuotaStates: Record; - accountQuotaEntriesByAccount: Map; - onMetricChange: (metric: AccountSortMetric) => void; - onToggleAccount: (accountId: string, account: string) => void; - onRefreshQuota: (account: string) => void; -}) { - const ACCOUNT_CARD_MIN_WIDTH = 330; - const ACCOUNT_CARD_GAP = 16; - const ROWS_PER_PAGE = 2; - - const [cardPage, setCardPage] = useState(0); - const [gridEl, setGridEl] = useState(null); - const gridRef = useCallback((el: HTMLDivElement | null) => setGridEl(el), []); - const [gridCols, setGridCols] = useState(3); - const [accountSearch, setAccountSearch] = useState(''); - const [accountProviderFilter, setAccountProviderFilter] = useState('all'); - const [accountHealthFilter, setAccountHealthFilter] = useState<'all' | AccountHealthTone>('all'); - - useEffect(() => { - if (!gridEl) return; - const update = () => { - const cols = Math.max(1, Math.floor((gridEl.clientWidth + ACCOUNT_CARD_GAP) / (ACCOUNT_CARD_MIN_WIDTH + ACCOUNT_CARD_GAP))); - setGridCols((current) => (current === cols ? current : cols)); - }; - update(); - const observer = new ResizeObserver(update); - observer.observe(gridEl); - return () => observer.disconnect(); - }, [gridEl]); - - const providerOptions = useMemo(() => { - const set = new Set(); - rows.forEach((row) => row.providers.forEach((p) => { if (p && p !== '-') set.add(p); })); - return Array.from(set).sort(); - }, [rows]); - - const filteredRows = useMemo(() => { - const query = accountSearch.trim().toLowerCase(); - return rows.filter((row) => { - if (query) { - const haystack = [row.accountMasked, row.account, ...row.authLabels, ...row.providers].join(' ').toLowerCase(); - if (!haystack.includes(query)) return false; - } - if (accountProviderFilter !== 'all') { - if (!row.providers.includes(accountProviderFilter)) return false; - } - if (accountHealthFilter !== 'all') { - if (getAccountHealthTone(row) !== accountHealthFilter) return false; - } - return true; - }); - }, [rows, accountSearch, accountProviderFilter, accountHealthFilter]); - - const hasActiveFilters = accountSearch.trim() !== '' || accountProviderFilter !== 'all' || accountHealthFilter !== 'all'; - - const itemsPerPage = gridCols * ROWS_PER_PAGE; - const totalPages = Math.max(1, Math.ceil(filteredRows.length / itemsPerPage)); - const safePageIndex = Math.min(cardPage, totalPages - 1); - const visibleRows = useMemo( - () => filteredRows.slice(safePageIndex * itemsPerPage, (safePageIndex + 1) * itemsPerPage), - [filteredRows, itemsPerPage, safePageIndex] - ); - - const accountStatusRange = useMemo( - () => buildAccountStatusRange(rows, range), - [rows, range] - ); - - const accountStatusDataById = useMemo(() => { - const entries = visibleRows.map((row) => [row.id, buildAccountStatusData(row.rows ?? [], accountStatusRange)] as const); - return new Map(entries); - }, [accountStatusRange, visibleRows]); - - useEffect(() => { - setCardPage(0); - }, [accountSearch, accountProviderFilter, accountHealthFilter, metric, range, itemsPerPage]); - - return ( - <> -
-
-

{t('monitoring.account_stats_title')}

-

{t('monitoring.account_stats_desc')}

-
- -
-
- {TIME_RANGE_OPTIONS.map((option) => ( - - ))} -
- -
-
- - -
-
- ) => setAccountSearch(event.target.value)} - placeholder={t('monitoring.search_account')} - className={styles.accountStatsSearchInput} - rightElement={} - aria-label={t('monitoring.search_account')} - /> - {providerOptions.length > 0 && ( - - )} - - {hasActiveFilters && ( - - )} -
-
- {ACCOUNT_SORT_OPTIONS.map((option) => ( - - ))} -
-
- - {filteredRows.length > 0 ? ( - <> -
- {visibleRows.map((row) => { - const statusData = accountStatusDataById.get(row.id) ?? buildAccountStatusData([], accountStatusRange); - return ( - onToggleAccount(row.id, row.account)} - onRefreshQuota={() => onRefreshQuota(row.account)} - /> - ); - })} -
- {totalPages > 1 && ( -
- -
- {t('auth_files.pagination_info', { - current: safePageIndex + 1, - total: totalPages, - count: filteredRows.length, - defaultValue: `${safePageIndex + 1} / ${totalPages} · ${filteredRows.length}`, - })} -
- -
- )} - - ) : ( -
{hasActiveFilters ? t('monitoring.no_matching_accounts') : emptyText}
- )} -
- - ); -} - -function StatusBadge({ tone, children }: { tone: MonitoringStatusTone; children: ReactNode }) { - return {children}; -} - -function RealtimeErrorDetailsPanel({ - row, - t, - language, -}: { - row: RealtimeLogRow; - t: ReturnType['t']; - language?: string; -}) { - const categoryText = translateRealtimeErrorCategory(row.errorCategoryKey, t, language); - const statusText = buildRealtimeStatusLabel(row, t('monitoring.result_failed')); - const summaryText = row.errorMessage - ? compactRealtimeErrorMessage(row.errorMessage, 220) - : row.errorSummary || row.diagnosticText || categoryText; - const detailItems = [ - { label: translateRealtimeErrorText('http_status', t, language), value: row.statusCode !== null ? String(row.statusCode) : '-' }, - { label: translateRealtimeErrorText('error_code', t, language), value: row.errorCode || '-' }, - { label: translateRealtimeErrorText('upstream_request_id', t, language), value: row.upstreamRequestId || '-' }, - { label: translateRealtimeErrorText('retry_after', t, language), value: row.retryAfter || '-' }, - ].filter((item) => item.value !== '-'); - - return ( -
-
-
- {statusText} - {categoryText} -
- {summaryText} -
- {row.errorMessage ? ( -
- {translateRealtimeErrorText('error_message', t, language)} -
{compactRealtimeErrorMessage(row.errorMessage, 1200)}
-
- ) : null} - {detailItems.length > 0 ? ( -
- {detailItems.map((item) => ( -
- {item.label} - {maskSensitiveText(item.value)} -
- ))} -
- ) : null} -
- ); -} - -function RecentPattern({ - pattern, - variant = 'default', - label, -}: { - pattern: boolean[]; - variant?: 'default' | 'plain'; - label?: string; -}) { - const normalized = pattern.length > 0 ? pattern : Array.from({ length: 10 }, () => true); - const successCount = normalized.filter(Boolean).length; - const failureCount = normalized.length - successCount; - const ariaLabel = label ?? `Recent ${normalized.length} requests: ${successCount} succeeded, ${failureCount} failed`; - const containerClassName = [ - styles.patternBars, - variant === 'plain' ? styles.patternBarsPlain : '', - ] - .filter(Boolean) - .join(' '); - const barClassName = [styles.patternBar, variant === 'plain' ? styles.patternBarPlain : ''] - .filter(Boolean) - .join(' '); - - return ( -
- {normalized.map((item, index) => ( -
- ); -} - export function MonitoringCenterPage() { const { t, i18n } = useTranslation(); const config = useConfigStore((state) => state.config); @@ -2353,48 +1214,6 @@ export function MonitoringCenterPage() { }); }, [observedPriceModels, priceRules]); - const filteredPriceRuleTargets = useMemo(() => { - const query = priceRuleSearch.trim().toLowerCase(); - if (!query) return priceRuleTargets; - return priceRuleTargets.filter((item) => { - const source = item.rule ? `${item.rule.sourceProvider ?? ''}/${item.rule.sourceModel ?? ''}` : ''; - return `${item.model} ${source}`.toLowerCase().includes(query); - }); - }, [priceRuleSearch, priceRuleTargets]); - - const selectedPriceTarget = useMemo( - () => priceRuleTargets.find((item) => item.model === priceModel) ?? null, - [priceModel, priceRuleTargets] - ); - - const configuredPriceRuleCount = priceRuleTargets.filter((item) => Boolean(item.rule)).length; - const unconfiguredPriceRuleCount = priceRuleTargets.length - configuredPriceRuleCount; - const priceSyncStatus = isPriceSyncing ? 'syncing' : priceSyncState.status; - const unmatchedPriceModels = priceSyncResult?.unmatched ?? priceSyncState.unmatchedModels ?? []; - const unmatchedPriceModelCount = priceSyncResult ? unmatchedPriceModels.length : (priceSyncState.unmatched ?? unmatchedPriceModels.length); - const priceSyncChanges = useMemo(() => priceSyncResult?.changes ?? [], [priceSyncResult]); - const priceSyncLockedOverrideSet = useMemo(() => new Set(priceSyncLockedOverrides), [priceSyncLockedOverrides]); - const priceSyncChangeCounts = useMemo(() => { - const counts: Record = { added: 0, updated: 0, overridden: 0, locked: 0, unmatched: 0 }; - priceSyncChanges.forEach((change) => { - const action = change.action === 'locked' && priceSyncLockedOverrideSet.has(change.model) ? 'overridden' : change.action; - counts[action] += 1; - }); - return counts; - }, [priceSyncChanges, priceSyncLockedOverrideSet]); - const filteredPriceSyncChanges = useMemo( - () => priceSyncChangeFilter === 'all' - ? priceSyncChanges - : priceSyncChanges.filter((change) => { - const action = change.action === 'locked' && priceSyncLockedOverrideSet.has(change.model) ? 'overridden' : change.action; - return action === priceSyncChangeFilter; - }), - [priceSyncChangeFilter, priceSyncChanges, priceSyncLockedOverrideSet] - ); - const lockedPriceSyncChanges = useMemo(() => priceSyncChanges.filter((change) => change.action === 'locked'), [priceSyncChanges]); - const allLockedPriceSyncChangesSelected = lockedPriceSyncChanges.length > 0 - && lockedPriceSyncChanges.every((change) => priceSyncLockedOverrideSet.has(change.model)); - const selectedFiltersCount = [selectedProvider, selectedModel, selectedApiKey, selectedStatus].filter( (value) => value !== 'all' @@ -2736,7 +1555,7 @@ export function MonitoringCenterPage() { }, tiers: priceDraft.tiers .map((tier) => ({ - contextSize: parseNonNegativeInteger(tier.contextSize), + contextSize: parsePriceContextSize(tier.contextSize), input: parsePriceValue(tier.input), output: parsePriceValue(tier.output), cacheRead: parsePriceValue(tier.cacheRead), @@ -3245,574 +2064,54 @@ export function MonitoringCenterPage() { ) : null} - { - if (!isMonitoringStatisticsResetting) setIsMonitoringSettingsOpen(false); - }} - title={t('usage_stats.monitoring_settings')} - width={760} - className={styles.monitorModal} - > -
-
-
- {t('usage_stats.monitoring_settings_retention_title')} - {t('usage_stats.monitoring_settings_retention_desc')} -
- -
- -
-
- {t('usage_stats.monitoring_settings_webdav_title')} - {t('usage_stats.monitoring_settings_webdav_desc')} -
- -
- - - - - -
- {t('usage_stats.monitoring_settings_webdav_hint')} -
- -
-
- {t('usage_stats.monitoring_settings_data_title')} - {t('usage_stats.monitoring_settings_data_desc')} -
-
-
- {t('usage_stats.monitoring_settings_data_count')} - {formatCompactNumber(Number(usage?.total_requests) || 0)} -
- -
-
- -
- - -
-
-
- - setIsPriceModalOpen(false)} - title={t('usage_stats.model_price_settings')} - width={960} - className={`${styles.monitorModal} ${styles.priceManagerModal}`} - > -
-
- - -
- - {priceManagementView === 'rules' ? ( -
- - -
- {selectedPriceTarget ? ( - <> -
-
-

{selectedPriceTarget.model}

- {t('usage_stats.model_price_model_scope')} -
-
- - {t(selectedPriceTarget.rule ? 'usage_stats.model_price_configured' : 'usage_stats.model_price_unconfigured')} - - {selectedPriceTarget.rule?.source ? {selectedPriceTarget.rule.source} : null} -
-
- -
-
-
-

{t('usage_stats.model_price_base_rates')}

- USD / 1M -
-
- {([ - ['input', 'usage_stats.model_price_input'], - ['output', 'usage_stats.model_price_output'], - ['cacheRead', 'usage_stats.model_price_cache_read'], - ['cacheWrite', 'usage_stats.model_price_cache_write'], - ] as const).map(([field, label]) => ( - - ))} -
-
- -
-
-
-

{t('usage_stats.model_price_context_tier')}

- {t('usage_stats.model_price_tier_count', { count: priceDraft.tiers.length })} -
- -
-
- {priceDraft.tiers.map((tier, index) => ( -
- {index + 1} - - - - - - -
- ))} - {priceDraft.tiers.length === 0 ? ( -
{t('usage_stats.model_price_tier_empty')}
- ) : null} -
-
-
- -
-
- {selectedPriceTarget.rule ? ( - - ) : null} -
-
- - -
-
- - ) : ( -
{t('usage_stats.model_price_select_empty')}
- )} -
-
- ) : ( -
-
-
- -
-

{t(`usage_stats.model_price_sync_state_${priceSyncStatus}`, { defaultValue: priceSyncStatus })}

- - {priceSyncState.lastSuccessMs - ? t('usage_stats.model_price_last_sync', { value: formatShortDateTime(priceSyncState.lastSuccessMs) }) - : t('usage_stats.model_price_sync_never')} - -
-
-
- - -
-
- -
- {([ - ['matched', priceSyncResult?.matched ?? priceSyncState.matched ?? 0], - ['added', priceSyncResult?.added ?? priceSyncState.added ?? 0], - ['updated', priceSyncResult?.updated ?? priceSyncState.updated ?? 0], - ['unmatched', unmatchedPriceModelCount], - ] as const).map(([key, value]) => ( -
- {t(`usage_stats.model_price_sync_metric_${key}`)} - {formatCompactNumber(value)} -
- ))} -
- - {priceSyncState.error ?
{priceSyncState.error}
: null} - - {priceSyncResult ? ( -
-
-
-

{t(priceSyncResult.dryRun ? 'usage_stats.model_price_sync_preview_details' : 'usage_stats.model_price_sync_applied_details')}

- {t('usage_stats.model_price_sync_change_summary', { - added: priceSyncChangeCounts.added, - updated: priceSyncChangeCounts.updated, - overridden: priceSyncChangeCounts.overridden, - locked: priceSyncChangeCounts.locked, - unmatched: unmatchedPriceModelCount, - })} -
-
- {lockedPriceSyncChanges.length > 0 ? ( - - ) : null} -
- {(['all', 'added', 'updated', 'overridden', 'locked', 'unmatched'] as const).map((filter) => { - const count = filter === 'all' ? priceSyncChanges.length : priceSyncChangeCounts[filter]; - if (filter !== 'all' && count === 0) return null; - const filterLabel = filter === 'overridden' && priceSyncResult.dryRun - ? t('usage_stats.model_price_sync_override_selected') - : t(`usage_stats.model_price_sync_change_${filter}`); - return ( - - ); - })} -
-
-
- -
- {filteredPriceSyncChanges.map((change) => { - const rateChanges = MODEL_PRICE_SYNC_RATE_FIELDS.filter(([field]) => ( - change.after && (!change.before || change.before.base[field] !== change.after.base[field]) - )); - const beforeTierCount = change.before?.tiers?.length ?? 0; - const afterTierCount = change.after?.tiers?.length ?? 0; - const overrideSelected = change.action === 'locked' && priceSyncLockedOverrideSet.has(change.model); - const displayedAction = overrideSelected ? 'overridden' : change.action; - return ( -
-
- - {overrideSelected - ? t('usage_stats.model_price_sync_override_selected') - : t(`usage_stats.model_price_sync_change_${displayedAction}`)} - -
- {change.model} - - {change.sourceProvider - ? `${change.sourceProvider}/${change.sourceModel || change.model}` - : t('usage_stats.model_price_sync_change_no_source')} - {' · '} - {t('usage_stats.model_price_requests', { count: change.requests })} - -
-
- - {change.action !== 'unmatched' ? ( -
- {rateChanges.map(([field, label]) => ( -
- {t(label)} -
- {change.before ? {formatModelPriceRate(change.before.base[field])} : null} - {change.before ? : null} - {formatModelPriceRate(change.after?.base[field])} -
-
- ))} - {beforeTierCount !== afterTierCount ? ( -
- {t('usage_stats.model_price_context_tier')} -
- {beforeTierCount} - - {afterTierCount} -
-
- ) : null} - {rateChanges.length === 0 && beforeTierCount === afterTierCount ? ( - {t(change.action === 'locked' - ? overrideSelected - ? 'usage_stats.model_price_sync_override_selected_hint' - : 'usage_stats.model_price_sync_change_locked_hint' - : 'usage_stats.model_price_sync_change_metadata_hint')} - ) : null} - {change.action === 'locked' ? ( - - ) : null} -
- ) : ( - {t('usage_stats.model_price_sync_change_unmatched_hint')} - )} -
- ); - })} - {filteredPriceSyncChanges.length === 0 ? ( -
{t('usage_stats.model_price_sync_no_changes')}
- ) : null} -
-
- ) : unmatchedPriceModelCount > 0 ? ( -
-
-
-

{t('usage_stats.model_price_sync_unmatched')}

- {unmatchedPriceModelCount} -
-
-
- {unmatchedPriceModels.map((item) => ( -
- - {item.model} - {item.alias ? {item.alias} : null} - - {t('usage_stats.model_price_requests', { count: item.requests })} -
- ))} -
-
- ) : null} - -
-
- {t('usage_stats.model_price_sync_schedule_title')} - {t('usage_stats.model_price_sync_schedule_desc')} -
-
- - - -
-
-
- )} -
-
+ + + ); } diff --git a/cliproxyapi-pro-management/overlay/tests/accountQuota.test.ts b/cliproxyapi-pro-management/overlay/tests/accountQuota.test.ts new file mode 100644 index 0000000..3c7bab9 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/tests/accountQuota.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from 'bun:test'; +import { + buildAccountQuotaTargetsByAccount, + getAccountQuotaConfig, +} from '../src/features/monitoring/accountQuota'; +import type { MonitoringEventRow } from '../src/features/monitoring/hooks/useMonitoringData'; +import type { AuthFileItem } from '../src/types'; + +const authFile = (provider: string, name: string): AuthFileItem => ({ + provider, + name, +} as AuthFileItem); + +describe('monitoring account quota boundaries', () => { + test('supports only account providers rendered by the monitoring quota panel', () => { + expect(getAccountQuotaConfig(authFile('codex', 'codex.json'))?.type).toBe('codex'); + expect(getAccountQuotaConfig(authFile('xai', 'xai.json'))?.type).toBe('xai'); + expect(getAccountQuotaConfig(authFile('gemini-cli', 'gemini.json'))).toBeUndefined(); + }); + + test('groups and de-duplicates quota targets by account and credential identity', () => { + const source = { + authIndex: 'auth-1', + account: 'owner@example.com', + authLabel: 'Primary', + } as MonitoringEventRow; + const grouped = buildAccountQuotaTargetsByAccount( + [source, source], + new Map([['auth-1', authFile('codex', 'codex.json')]]) + ); + + expect(grouped.get('owner@example.com')).toHaveLength(1); + expect(grouped.get('owner@example.com')?.[0]).toMatchObject({ + authIndex: 'auth-1', + authLabel: 'Primary', + fileName: 'codex.json', + }); + }); +}); diff --git a/cliproxyapi-pro-management/overlay/tests/modelPricePresentation.test.ts b/cliproxyapi-pro-management/overlay/tests/modelPricePresentation.test.ts new file mode 100644 index 0000000..ba2e12d --- /dev/null +++ b/cliproxyapi-pro-management/overlay/tests/modelPricePresentation.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from 'bun:test'; +import { + createPriceDraft, + formatDeltaPercent, + parsePriceValue, +} from '../src/features/monitoring/modelPricePresentation'; + +describe('model price presentation model', () => { + test('creates editable strings without mutating the source rule', () => { + const draft = createPriceDraft({ + id: 1, + version: 2, + provider: '', + model: 'gpt-test', + source: 'manual', + base: { input: 1, output: 2, cacheRead: 0.5, cacheWrite: 0.75 }, + tiers: [{ contextSize: 1000, input: 3, output: 4, cacheRead: 1, cacheWrite: 1.5 }], + }); + + expect(draft.input).toBe('1'); + expect(draft.tiers[0]).toMatchObject({ contextSize: '1000', output: '4' }); + }); + + test('normalizes invalid rates and reports rounded deltas', () => { + expect(parsePriceValue('-1')).toBe(0); + expect(parsePriceValue('not-a-number')).toBe(0); + expect(formatDeltaPercent(1.5, 1)).toBe('+50.0%'); + expect(formatDeltaPercent(0, 0)).toBe('0.0%'); + }); +}); diff --git a/cliproxyapi-pro-management/overlay/tests/monitoringSettings.test.ts b/cliproxyapi-pro-management/overlay/tests/monitoringSettings.test.ts new file mode 100644 index 0000000..bd5fd40 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/tests/monitoringSettings.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from 'bun:test'; +import { + buildMonitoringSettingsFromDraft, + createMonitoringSettingsDraft, +} from '../src/features/monitoring/monitoringSettings'; + +describe('monitoring settings form model', () => { + test('uses stable defaults for a new settings form', () => { + expect(createMonitoringSettingsDraft()).toMatchObject({ + retentionDays: '0', + webdavEnabled: false, + webdavIntervalMinutes: '1440', + modelPriceSyncEnabled: false, + modelPriceSyncIntervalMinutes: '1440', + }); + }); + + test('normalizes numeric fields and trims connection identity fields', () => { + const settings = buildMonitoringSettingsFromDraft({ + retentionDays: '-1', + webdavEnabled: true, + webdavIntervalMinutes: '0', + webdavRetentionDays: '7', + webdavUrl: ' https://example.com/dav ', + webdavUsername: ' owner ', + webdavPassword: ' preserve spaces ', + modelPriceSyncEnabled: true, + modelPriceSyncIntervalMinutes: '60', + }); + + expect(settings.retentionDays).toBe(0); + expect(settings.webdav.intervalMinutes).toBe(1440); + expect(settings.webdav.url).toBe('https://example.com/dav'); + expect(settings.webdav.username).toBe('owner'); + expect(settings.webdav.password).toBe(' preserve spaces '); + expect(settings.modelPriceSync.intervalMinutes).toBe(60); + }); +}); diff --git a/cliproxyapi-pro-management/tests/test_monitoring_module_boundaries.py b/cliproxyapi-pro-management/tests/test_monitoring_module_boundaries.py index 3cc6f94..e1e5672 100644 --- a/cliproxyapi-pro-management/tests/test_monitoring_module_boundaries.py +++ b/cliproxyapi-pro-management/tests/test_monitoring_module_boundaries.py @@ -10,13 +10,16 @@ class MonitoringModuleBoundariesTest(unittest.TestCase): def test_page_remains_a_controller_instead_of_reabsorbing_extracted_logic(self) -> None: source = PAGE_PATH.read_text() - self.assertLess(len(source.splitlines()), 4000) + self.assertLess(len(source.splitlines()), 2300) self.assertNotIn('const buildUsageTrendAnalytics =', source) self.assertNotIn('const buildRealtimeLogPageRows =', source) self.assertNotIn('const normalizeRealtimeLogColumns =', source) self.assertNotIn('function RealtimeCostCell(', source) self.assertNotIn('function UsageTrendPanel(', source) self.assertNotIn('function MonitoringHealthStatusBar(', source) + self.assertNotIn('function AccountStatsPanel(', source) + self.assertNotIn('open={isMonitoringSettingsOpen}', source) + self.assertNotIn('open={isPriceModalOpen}', source) def test_extracted_modules_own_their_domain_contracts(self) -> None: analytics = (OVERLAY_ROOT / 'features/monitoring/monitoringAnalytics.ts').read_text() @@ -30,6 +33,13 @@ def test_extracted_modules_own_their_domain_contracts(self) -> None: self.assertIn('export const normalizeRealtimeLogColumns', preferences) self.assertIn('export const buildAccountStatusData', health) + def test_monitoring_feature_owns_shared_styles(self) -> None: + feature_root = OVERLAY_ROOT / 'features/monitoring' + self.assertTrue((feature_root / 'monitoring.module.scss').is_file()) + self.assertFalse((OVERLAY_ROOT / 'pages/MonitoringCenterPage.module.scss').exists()) + for component in (feature_root / 'components').glob('*.tsx'): + self.assertNotIn("@/pages/MonitoringCenterPage.module.scss", component.read_text()) + if __name__ == '__main__': unittest.main() diff --git a/cliproxyapi-pro-management/tests/test_monitoring_toolbar_customization.py b/cliproxyapi-pro-management/tests/test_monitoring_toolbar_customization.py index f80d488..fb1dc7d 100644 --- a/cliproxyapi-pro-management/tests/test_monitoring_toolbar_customization.py +++ b/cliproxyapi-pro-management/tests/test_monitoring_toolbar_customization.py @@ -6,13 +6,21 @@ Path(__file__).resolve().parents[1] / 'overlay/src/pages/MonitoringCenterPage.tsx' ) -STYLE_PATH = PAGE_PATH.with_suffix('.module.scss') +STYLE_PATH = ( + Path(__file__).resolve().parents[1] + / 'overlay/src/features/monitoring/monitoring.module.scss' +) +STYLE_DIR = STYLE_PATH.parent / 'styles' REALTIME_HOOK_PATH = ( Path(__file__).resolve().parents[1] / 'overlay/src/features/monitoring/hooks/useRealtimeLogData.ts' ) +def read_monitoring_styles() -> str: + return '\n'.join(path.read_text() for path in sorted(STYLE_DIR.glob('*.scss'))) + + class MonitoringToolbarCustomizationTest(unittest.TestCase): def test_monitoring_settings_button_keeps_a_stable_label_while_loading(self) -> None: source = PAGE_PATH.read_text() @@ -42,7 +50,7 @@ def test_realtime_logs_pause_auto_refresh_during_browsing(self) -> None: def test_realtime_logs_restore_the_internal_scroll_anchor(self) -> None: source = PAGE_PATH.read_text() hook_source = REALTIME_HOOK_PATH.read_text() - styles = STYLE_PATH.read_text() + styles = read_monitoring_styles() self.assertIn("data-realtime-row-id={row.id}", source) self.assertIn("pendingScrollSnapshotRef", hook_source) @@ -59,7 +67,7 @@ def test_realtime_follow_control_and_pending_update_action_are_present(self) -> def test_realtime_follow_refresh_does_not_change_outer_layout_height(self) -> None: source = PAGE_PATH.read_text() - styles = STYLE_PATH.read_text() + styles = read_monitoring_styles() self.assertIn("pendingRealtimeEventCount > 0 && realtimeLogAutoRefreshPaused", source) self.assertIn("className={styles.realtimeTableShell}", source) diff --git a/docs/code-review/2026-07.md b/docs/code-review/2026-07.md index 98880ef..9f3b022 100644 --- a/docs/code-review/2026-07.md +++ b/docs/code-review/2026-07.md @@ -22,6 +22,7 @@ The pinned PR baseline lives in `compatibility/upstream.env`. Pull requests and | 8 | Stable server-side log search and realtime controller extraction | Completed | | 9 | Reproducible Core builds and bidirectional overlay provenance gates | Completed | | 10 | Monitoring page decomposition, duplicate removal, and module-boundary guards | Completed | +| 11 | Monitoring controller, modal, account-quota, and stylesheet decomposition | Completed | ## Severity @@ -68,12 +69,27 @@ The pinned PR baseline lives in `compatibility/upstream.env`. Pull requests and | FRONTEND-003 | P2 | Monitoring metadata and aggregates | Metadata and aggregate hooks did not include server identity in their request lifecycle | Equal cursors across servers or out-of-order refreshes could leave channels, auth metadata, rankings, or summaries from the previous connection | Switch between servers with the same latest usage ID while a refresh is in flight | Gate loads on authenticated connection identity, invalidate prior requests, clear cross-server data, and suppress stale responses | Fresh-management type, lint, and integration build validation | Resolved | | FRONTEND-004 | P2 | Account inspection transport | The inspection WebSocket was opened once with no close/error recovery or polling fallback | A transient proxy or network disconnect left progress and results stale until page remount or a manual action | Close the active WebSocket after the initial snapshot | Add capped exponential reconnect, five-second status polling while disconnected, request sequencing, and last-good snapshot preservation | WebSocket URL, credential transport, and reconnect-backoff tests | Resolved | | FRONTEND-005 | P2 | Routing runtime polling | Fifteen-second runtime polls could overlap and race with manual refresh, save, or release responses | An older runtime response could overwrite the latest protected-account/event state | Delay a poll response until after a release response | Prevent overlapping polls and apply only the latest request generation across load, poll, save, refresh, and release paths | Type checking, lint, and production build | Resolved | -| FRONTEND-006 | P2 | Monitoring maintainability | Monitoring, inspection, and routing pages had grown to roughly 6.4k, 3.6k, and 1k lines, and the monitoring page still owned analytics, persistence migration, realtime presentation, and large panels after earlier transport extraction | Further realtime and analytics changes were difficult to test independently and increased regression risk | Inspect page sizes, repeated helper ownership, and dependency direction | Extract monitoring analytics/aggregates, realtime preferences/presentation, account health, and large analytics panels into feature modules; make the page consume those contracts and guard against reabsorption | Focused Bun tests, source-boundary tests, and full Management validation | Mitigated | +| FRONTEND-006 | P2 | Monitoring maintainability | Monitoring, inspection, and routing pages had grown to roughly 6.4k, 3.6k, and 1k lines, and the monitoring page still owned analytics, account quota adaptation, settings/price modals, realtime presentation, and a 6.2k-line page stylesheet | Further realtime, pricing, and account-view changes were difficult to test independently and increased regression risk | Inspect page/component sizes, repeated helper ownership, and page-to-feature dependency direction | Extract monitoring analytics/aggregates, account quota/health, realtime preferences/presentation, controlled settings/price components, account panels, and ordered Sass partials; make the page consume those contracts and guard against reabsorption | Focused Bun tests, source/style-boundary tests, and full Management validation | Mitigated | | LIFECYCLE-001 | P2 | Management handler lifecycle | Account-inspection, routing reconciliation, login-attempt cleanup, named usage registration, and embedded-usage callbacks outlived a stopped server | Embedders that repeatedly constructed and stopped servers retained goroutines and handler references | Construct and stop management handlers repeatedly in one process | Give each handler a cancellable lifecycle, wait for scheduled and active work, remove global registrations, and invoke shutdown from `Server.Stop` | Handler shutdown, named-plugin ownership, full Core tests, and race detector | Resolved | | FRONTEND-007 | P2 | API request cancellation | Connection generations rejected stale Axios responses but did not abort the underlying requests | Superseded requests continued consuming client and server resources until response or timeout | Hold an adapter request open and switch server credentials | Abort the previous connection controller and combine its signal with any caller-provided cancellation signal | Physical connection-abort and caller-cancellation tests | Resolved | | FRONTEND-008 | P2 | Inspection reconnect detail recovery | WebSocket downtime used summary polling, so missed individual log entries were not refreshed immediately when the socket reconnected | Operators could briefly see an incomplete log page after transport recovery | Drop the inspection socket after a live log and reconnect it | Refresh the current filtered result/log detail page immediately on socket open while retaining the last good snapshot on failure | Management transport tests, type checking, lint, and production build | Resolved | | FRONTEND-009 | P2 | Realtime log search and ownership | Account-name search derived auth indexes only from recently loaded event rows and then suppressed raw event-text search; request cancellation, stable cursors, auto-follow, and scroll restoration all remained embedded in the page | Older events for a known account could be omitted, raw matches could disappear when account metadata also matched, and further paging changes had a large regression surface | Search for an account absent from the recent event snapshot while another event contains the same raw term, then paginate the combined results | Build auth matches from complete auth-file metadata, OR them with raw text on the server, preserve both conditions in the stable cursor, and extract the realtime controller into a dedicated hook | Combined-search cursor test, auth metadata search tests, controller source guards, full Core/Management validation, and race detector | Resolved | +## Package 11 Residual Risks + +- `MonitoringCenterPage.tsx` is now 2117 lines, down from 6284 before Packages 10-11 (about 66%). It remains the intentional page-level coordinator for filtering, data-source selection, refresh/import/reset commands, realtime table columns, and modal state. +- `AccountInspectionPage.tsx` remains a roughly 3.5k-line feature surface and is isolated as Package 12 rather than mixed into this monitoring PR. +- Client fallback analytics remain bounded by `ACCOUNT_STATS_ANALYTICS_ROW_LIMIT` for compatibility with servers where aggregate data is unavailable. Removing that path requires an explicit minimum-server-version/API decision. +- Monitoring styles are feature-owned and split into seven ordered Sass partials. They remain one CSS module so cross-panel tokens and responsive selectors preserve their existing cascade; fully component-local styling would require a dedicated visual regression package. + +## Package 11 Validation Results + +- Repository validation: 23 Management customization tests and two reproducible archive tests passed. Boundary guards keep the monitoring page below 2300 lines, prevent account/settings/price implementations from returning to it, and enforce feature ownership of monitoring styles. +- Management `v1.18.5`: bidirectional overlay replacement preflight, repeated-application invariance, 108 Bun tests, ESLint with no warnings, TypeScript checking, and the production single-file build passed on a fresh checkout after the Sass split. +- Core `v7.2.94`: guarded-source preflight, rejected reapplication invariance, embedded usage tests, management handlers, plugin host/store, Redis queue, auth scheduler tests, and the server build passed on a fresh checkout. +- Focused coverage verifies provider-scoped account-quota targeting/de-duplication, monitoring-settings defaults and normalization, and model-price draft/rate behavior. +- The 6213-line page stylesheet is now a seven-line feature entrypoint plus ordered base, analytics, accounts, management, realtime, utilities, and responsive partials; production CSS size remained stable through the fresh build. + ## Package 10 Residual Risks - `MonitoringCenterPage.tsx` is reduced from 6284 to 3818 lines (about 39%), but it still coordinates account quota loading, price management, backup/reset workflows, realtime table state, and modal composition. Further extraction is incremental P2 maintainability work rather than a release blocker.