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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ export interface ApiFormatStatsItem {
|
||||
// 请求状态类型
|
||||
// 日期范围参数
|
||||
export interface DateRangeParams {
|
||||
from?: string
|
||||
to?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
preset?: string
|
||||
|
||||
Reference in New Issue
Block a user