feat: revamp analytics dashboards and harden database migrations

Add dashboard and overview analytics, health monitoring, provider expense tracking, and announcement updates across the gateway and frontend.

Keep schema migrations free of historical backfills while preserving automatic backfill execution. Bound migration deadlines, run schema preparation before Compose replacement, and anonymize deleted dashboard users.

Include the current documentation cleanup and regression coverage.
This commit is contained in:
elky
2026-10-01 11:48:17 +08:00
parent 60b89cc840
commit 066ea87d72
327 changed files with 31728 additions and 20645 deletions
+301
View File
@@ -0,0 +1,301 @@
import apiClient from './client'
import type { GatewayMetricsSummary, AdminMonitoringResilienceStatus } from './monitoring'
import type { ProviderPerformanceResponse } from './admin'
export interface OverviewRange {
from: string
to: string
timezone: string
}
export interface OverviewQuery extends OverviewRange {
user_id?: string
api_key_id?: string
request_type?: string
credential_owner_id?: string
attribution_kind?: string
model?: string
provider_id?: string
api_format?: string
endpoint_kind?: string
is_stream?: boolean
has_format_conversion?: boolean
slow_threshold_ms?: number
status?: string
search?: string
account_status?: string
usage_status?: string
sort?: string
order?: 'asc' | 'desc'
group_by?: string
amount_basis?: string
granularity?: 'hour' | 'day'
limit?: number
offset?: number
payment_limit?: number
payment_offset?: number
}
export interface OverviewAmount {
value: string | null
currency: string
basis: string
status: 'known' | 'known_subtotal' | 'unknown' | 'estimated' | string
}
export interface OverviewMetrics {
request_count: number
successful_request_count: number
failed_request_count: number
cancelled_request_count: number
in_flight_request_count: number
unclassified_failure_count: number
input_tokens: number | null
output_tokens: number | null
total_tokens: number | null
usage_source?: 'reported' | 'estimated' | 'mixed' | 'unknown'
usage_source_counts?: { reported: number; estimated: number; mixed: number; unknown: number }
requests_per_second?: number
requests_per_minute?: number
tokens_per_minute?: number | null
window_seconds?: number
usage_active_users?: number
enabled_users?: number
success_rate: { value: number | null; numerator: number; denominator: number }
latency_ms: { avg: number | null; p50: number | null; p95: number | null; p99: number | null; sample_count: number }
rated_amount: OverviewAmount
billable_amount: OverviewAmount
quota_covered_amount: OverviewAmount
wallet_consumed_amount: OverviewAmount
wallet_debit_amount: OverviewAmount
}
export interface OverviewMeta {
schema_version: number
metric_version: string
scope: { kind: string; user_id?: string }
range: OverviewRange & { time_basis: string }
generated_at: string
data_through: string | null
read_revision: string
available_metrics?: string[]
projection?: {
projection_from: string | null
projection_through: string | null
dirty_bucket_count: number
missing_bucket_count: number
read_enabled: boolean
}
coverage: {
status: string
request_count: number
usage_available_count: number
pricing_available_count: number
settled_count: number
attribution_available_count?: number
unrecoverable_bucket_count?: number
}
}
export interface OverviewResponse<T> { meta: OverviewMeta; data: T }
export type OverviewDashboardTotals = Pick<OverviewMetrics, 'request_count' | 'total_tokens' | 'billable_amount' | 'enabled_users'>
export interface OverviewDashboard {
today: OverviewResponse<OverviewMetrics>
total: OverviewResponse<OverviewDashboardTotals>
history_complete: boolean | null
}
export type OverviewDashboardTotal = { status: 'pending' } | {
status: 'ready'
total: OverviewResponse<OverviewDashboardTotals>
history_complete: boolean | null
stale: boolean
}
export interface OverviewDashboardSummary {
stats_since: string
generated_at: string
timezone: string
today_from: string
window_seconds: number
today: {
request_count: number
input_tokens: number | null
output_tokens: number | null
total_tokens: number | null
billable_amount: OverviewAmount
active_users: number
cache_read_tokens: number | null
cache_creation_tokens: number | null
cache_input_tokens: number | null
avg_first_byte_ms: number | null
avg_response_ms: number | null
stream_requests: number
standard_requests: number
}
total: {
request_count: number
total_tokens: number | null
billable_amount: OverviewAmount
cache_read_tokens: number | null
cache_input_tokens: number | null
}
users: { total: number; created_today: number; deleted_today: number }
consecutive_active_days: number
active_days: number
activity_days: { date: string; requests: number }[]
concurrency: {
avg: number | null
peak: number | null
observed_from: string | null
observed_through: string | null
scope: 'node'
coverage: 'partial' | 'complete' | 'unavailable'
}
}
export interface OverviewPage<T> { items: T[]; total: number; limit: number; offset: number }
export interface OverviewSeriesPoint extends OverviewMetrics { bucket_start: string }
export interface OverviewBreakdown extends OverviewMetrics { id: string | null; label: string | null }
export interface OverviewDashboardCharts {
summary: OverviewMetrics
series: OverviewSeriesPoint[]
models: (OverviewBreakdown & { bucket_start: string })[]
providers: OverviewBreakdown[]
}
export interface OverviewEmployee extends OverviewMetrics {
user_id: string
username: string
email: string | null
is_active: boolean
last_used_at: string | null
active_days: number
finance?: OverviewUserFinance | null
}
export interface OverviewUserFinance {
wallet_balance: OverviewAmount
recharge_balance: OverviewAmount
gift_balance: OverviewAmount
recharge_amount: OverviewAmount
recharge_count: number
plan_purchase_amount: OverviewAmount
plan_purchase_count: number
gift_credit_amount: OverviewAmount
gift_credit_count: number
balance_time_basis: 'current'
payment_time_basis: 'credited_at'
}
export interface OverviewUserPayment {
id: string
order_no: string
kind: 'wallet_recharge' | 'plan_purchase' | 'gift_credit'
amount: OverviewAmount
payment_method: string
credited_at: string
}
export interface OverviewUsers extends OverviewPage<OverviewEmployee> {
summary?: OverviewMetrics & { user_count: number; active_user_count: number }
finance_summary?: OverviewUserFinance | null
}
export interface OverviewEmployeeDetail {
user: { id: string; username: string; email: string | null; is_active: boolean }
summary: OverviewMetrics
finance?: OverviewUserFinance | null
payments?: OverviewPage<OverviewUserPayment> | null
}
export interface OverviewConsumption {
id: string
request_id: string
started_at: string
user_id: string | null
model: string | null
provider: string | null
status: string
settlement_status: string
attribution_kind: string
rated_amount: OverviewAmount
billable_amount: OverviewAmount
quota_covered_amount: OverviewAmount
wallet_consumed_amount: OverviewAmount
wallet_debit_amount: OverviewAmount
}
export interface OverviewCosts {
summary: OverviewMetrics
timeseries?: OverviewSeriesPoint[]
supplier_estimated_cost: OverviewAmount
supplier_verified_cost: OverviewAmount
cache: { read_tokens: number | null; creation_tokens: number | null; read_cost: OverviewAmount; creation_cost: OverviewAmount; estimated_full_cost: OverviewAmount; estimated_savings: OverviewAmount; pricing_available_count?: number; request_count?: number }
forecast: { amount: OverviewAmount; method: string; status: string; sample_days: number; period_end: string | null }
}
export interface OverviewExecutionActivity {
observed_at: string
observed_from: string
window_seconds: number
observed_window_seconds: number
scope: { kind: 'node' }
coverage: 'complete' | 'partial'
providers: { provider_id: string; provider: string | null; requests_per_minute: number; current_concurrency: number }[]
models: { model: string | null; requests_per_minute: number; current_concurrency: number }[]
}
export interface OverviewLive {
observed_at: string | null
window_seconds: number | null
node_id: string | null
scope: { kind: string; node_ids?: string[] }
metrics?: GatewayMetricsSummary | null
metrics_text?: string | null
resilience: AdminMonitoringResilienceStatus | null
unavailable_sections: string[]
recent_activity?: OverviewResponse<OverviewMetrics>
execution_activity?: OverviewExecutionActivity | null
}
export interface OverviewModelPerformance {
model: string | null
request_count: number
success_count: number
error_count: number
success_rate: number | null
avg_first_byte_time_ms: number | null
avg_output_tps: number | null
avg_response_time_ms: number | null
}
export interface OverviewPerformance {
summary: OverviewMetrics
timeseries: OverviewSeriesPoint[]
providers: ProviderPerformanceResponse | null
models?: OverviewModelPerformance[]
errors: { reason: string; count: number }[]
}
async function get<T>(path: string, params?: OverviewQuery, signal?: AbortSignal): Promise<OverviewResponse<T>> {
return (await apiClient.get<OverviewResponse<T>>(`/api/admin/overview/${path}`, { params, signal })).data
}
export const overviewApi = {
async dashboardSummary(timezone: string, signal?: AbortSignal): Promise<OverviewDashboardSummary> {
return (await apiClient.get<OverviewDashboardSummary>('/api/admin/overview/dashboard/summary', { params: { timezone }, signal })).data
},
async dashboard(timezone: string, signal?: AbortSignal): Promise<OverviewDashboard> {
return (await apiClient.get<OverviewDashboard>('/api/admin/overview/dashboard', { params: { timezone }, signal })).data
},
async dashboardTotal(timezone: string, signal?: AbortSignal): Promise<OverviewDashboardTotal> {
return (await apiClient.get<OverviewDashboardTotal>('/api/admin/overview/dashboard/total', { params: { timezone }, signal })).data
},
async dashboardCharts(range: OverviewRange, signal?: AbortSignal): Promise<OverviewResponse<OverviewDashboardCharts>> {
const { from, to, timezone } = range
return (await apiClient.get<OverviewResponse<OverviewDashboardCharts>>('/api/admin/overview/dashboard/charts', { params: { from, to, timezone, granularity: 'day' }, signal })).data
},
summary: (query: OverviewQuery, signal?: AbortSignal) => get<OverviewMetrics>('summary', query, signal),
timeseries: (query: OverviewQuery, signal?: AbortSignal) => get<{ items: OverviewSeriesPoint[]; granularity: string }>('timeseries', query, signal),
breakdown: (query: OverviewQuery, signal?: AbortSignal) => get<OverviewPage<OverviewBreakdown>>('breakdown', query, signal),
users: (query: OverviewQuery, signal?: AbortSignal) => get<OverviewUsers>('users', query, signal),
user: (id: string, query: OverviewQuery, signal?: AbortSignal) => get<OverviewEmployeeDetail>(`users/${encodeURIComponent(id)}`, query, signal),
consumption: (query: OverviewQuery, signal?: AbortSignal) => get<OverviewPage<OverviewConsumption>>('consumption', query, signal),
costs: (query: OverviewQuery, signal?: AbortSignal) => get<OverviewCosts>('costs', query, signal),
performance: (query: OverviewQuery, signal?: AbortSignal) => get<OverviewPerformance>('operations/performance', query, signal),
live: (signal?: AbortSignal) => get<OverviewLive>('operations/live', undefined, signal),
resources: (signal?: AbortSignal) => get<OverviewLive>('operations/resources', undefined, signal),
async exportCsv(path: 'users' | 'consumption' | 'breakdown', query: OverviewQuery, signal?: AbortSignal): Promise<Blob> {
const { limit: _limit, offset: _offset, ...filters } = query
return (await apiClient.get<Blob>(`/api/admin/overview/${path}`, {
params: { ...filters, format: 'csv' }, responseType: 'blob', signal,
})).data
},
}