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
@@ -1,12 +1,16 @@
<template>
<Card class="p-4 overflow-hidden">
<Card
class="p-4 overflow-hidden"
:title="tooltip"
>
<div class="flex items-center justify-between mb-3">
<p class="text-sm font-semibold">
{{ title }}
</p>
<div
v-if="hasData"
class="flex items-center gap-1 text-[11px] text-muted-foreground flex-shrink-0"
class="items-center gap-1 text-[11px] text-muted-foreground flex-shrink-0"
:class="compact ? 'hidden sm:flex' : 'flex'"
>
<span class="flex-shrink-0">少</span>
<div
@@ -36,6 +40,7 @@
v-else-if="hasData"
:data="data"
:show-header="false"
:compact="compact"
/>
<div
v-else
@@ -56,6 +61,8 @@ import type { ActivityHeatmap as ActivityHeatmapData } from '@/types/activity'
const props = defineProps<{
data: ActivityHeatmapData | null
title: string
tooltip?: string
compact?: boolean
isLoading?: boolean
hasError?: boolean
}>()
@@ -68,9 +68,11 @@ const props = withDefaults(defineProps<{
isAdmin: boolean
hours?: number
refreshIntervalMs?: number
data?: IntervalTimelineResponse | null
}>(), {
hours: 24, // 默认当天
refreshIntervalMs: 30000
refreshIntervalMs: 30000,
data: undefined,
})
const loading = ref(false)
@@ -292,6 +294,11 @@ const chartOptions = computed<ChartOptions<'scatter'>>(() => ({
async function loadData() {
const requestId = ++loadRequestId
if (props.data !== undefined) {
timelineData.value = props.data
loading.value = false
return
}
loading.value = true
try {
const limit = props.isAdmin ? ADMIN_TIMELINE_LIMIT : USER_TIMELINE_LIMIT
@@ -332,6 +339,7 @@ function stopRefreshTimer() {
}
function scheduleNextRefresh() {
if (props.data !== undefined) return
if (refreshTimer) return
if (!isPageVisible.value) return
if (!props.refreshIntervalMs || props.refreshIntervalMs <= 0) return
@@ -355,7 +363,7 @@ function handleVisibilityChange() {
scheduleNextRefresh()
}
watch([() => props.hours, () => props.isAdmin], () => {
watch([() => props.hours, () => props.isAdmin, () => props.data], () => {
void loadData()
stopRefreshTimer()
scheduleNextRefresh()
@@ -137,6 +137,18 @@ describe('useUsageData', () => {
}))
})
it('preserves precise overview scope and false performance filters in records and total requests', async () => {
const { loadRecords } = useUsageData({ isAdminPage: ref(true) })
getAllUsageRecordsMock.mockResolvedValueOnce({ records: [], total: 100, total_is_estimated: true })
const range = { from: '2026-09-10T16:20:42.000Z', to: '2026-09-10T17:20:42.000Z', timezone: 'Asia/Shanghai' }
const filters = { provider_id: 'provider-1', api_key_id: 'key-1', endpoint_kind: 'chat', request_type: 'standard', is_stream: false, has_format_conversion: false, slow_threshold_ms: 8000 }
await loadRecords({ page: 2, pageSize: 20 }, filters, range)
await flushMicrotasks()
expect(getAllUsageRecordsMock).toHaveBeenCalledWith(expect.objectContaining({ ...range, ...filters, offset: 20 }))
expect(getAllUsageRecordTotalMock).toHaveBeenCalledWith(expect.objectContaining({ ...range, ...filters }))
expect(getAllUsageRecordsMock.mock.calls[0][0].preset).toBeUndefined()
})
it('sends the WebSocket type filter to the user records endpoint before pagination', async () => {
const isAdminPage = ref(false)
const { loadRecords } = useUsageData({ isAdminPage })
@@ -36,6 +36,15 @@ export interface PaginationParams {
}
export interface FilterParams {
provider_id?: string
api_key_id?: string
request_id?: string
attribution_kind?: string
endpoint_kind?: string
request_type?: string
is_stream?: boolean
has_format_conversion?: boolean
slow_threshold_ms?: number
search?: string
user_id?: string
model?: string
@@ -399,6 +408,12 @@ export function useUsageData(options: UseUsageDataOptions) {
if (isAdminPage.value) {
// 管理员页面:使用管理员 API
for (const key of ['provider_id', 'api_key_id', 'request_id', 'attribution_kind', 'endpoint_kind', 'request_type'] as const) {
if (filters?.[key]) params[key] = filters[key]
}
for (const key of ['is_stream', 'has_format_conversion', 'slow_threshold_ms'] as const) {
if (filters?.[key] !== undefined) params[key] = filters[key]
}
if (filters?.user_id) {
params.user_id = filters.user_id
}
+2
View File
@@ -79,6 +79,8 @@ export interface ApiFormatStatsItem {
// 请求状态类型
// 日期范围参数
export interface DateRangeParams {
from?: string
to?: string
start_date?: string
end_date?: string
preset?: string