mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
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:
@@ -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
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user