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
@@ -0,0 +1,66 @@
<template>
<div
v-if="error"
role="alert"
class="flex flex-wrap items-center justify-between gap-2 border-l-2 border-destructive bg-destructive/5 px-3 py-2 text-sm"
>
<span class="min-w-0 break-words">{{ t('数据加载失败', 'Unable to load data') }}: {{ error }}</span>
<Button
variant="outline"
size="sm"
:disabled="loading"
@click="$emit('retry')"
>
{{ t('重试', 'Retry') }}
</Button>
</div>
<div
v-if="loading && !meta"
class="flex min-h-40 items-center justify-center gap-2 text-sm text-muted-foreground"
role="status"
>
<Loader2 class="h-4 w-4 animate-spin" />{{ t('加载中', 'Loading') }}
</div>
<div
v-if="meta"
class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground"
>
<span :class="partial ? 'text-amber-700 dark:text-amber-400' : ''">{{ partial ? t('部分数据', 'Partial data') : t('数据完整', 'Complete') }}</span>
<span>{{ t('计量覆盖', 'Usage coverage') }} {{ count(meta.coverage.usage_available_count) }}/{{ count(meta.coverage.request_count) }}</span>
<span>{{ t('定价覆盖', 'Pricing coverage') }} {{ count(meta.coverage.pricing_available_count) }}/{{ count(meta.coverage.request_count) }}</span>
<span>{{ t('已结算', 'Settled') }} {{ count(meta.coverage.settled_count) }}/{{ count(meta.coverage.request_count) }}</span>
<span
v-if="meta.coverage.unrecoverable_bucket_count"
class="text-amber-700 dark:text-amber-400"
>{{ t('原始数据已清理,无法恢复的时间桶', 'Source data removed; unrecoverable buckets') }} {{ count(meta.coverage.unrecoverable_bucket_count) }}</span>
<span
v-if="stale"
class="text-amber-700 dark:text-amber-400"
>{{ t('数据水位滞后', 'Data delayed') }}</span>
<span>{{ t('更新', 'Updated') }} {{ timestamp(meta.generated_at, meta.range.timezone) }}</span>
<span v-if="meta.data_through">{{ t('数据截至', 'Data through') }} {{ timestamp(meta.data_through, meta.range.timezone) }}</span>
</div>
<div
v-if="meta?.projection"
class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground"
>
<span v-if="!meta.projection.read_enabled">{{ t('原始事实读取', 'Reading source facts') }}</span>
<span>{{ t('聚合覆盖至', 'Aggregate coverage through') }} {{ timestamp(meta.projection.projection_through, meta.range.timezone) }}</span>
<span>{{ t('待重建', 'Awaiting rebuild') }} {{ count(meta.projection.dirty_bucket_count) }}</span>
<span>{{ t('未建', 'Not built') }} {{ count(meta.projection.missing_bucket_count) }}</span>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Loader2 } from 'lucide-vue-next'
import { Button } from '@/components/ui'
import type { OverviewMeta } from '@/api/overview'
import { count, timestamp } from '../format'
import { useOverviewI18n } from '../i18n'
const props = defineProps<{ meta?: OverviewMeta | null; loading?: boolean; error?: string | null }>()
defineEmits<{ retry: [] }>()
const { t } = useOverviewI18n()
const partial = computed(() => props.meta?.coverage.status !== 'complete')
const stale = computed(() => !!props.meta?.data_through && Date.parse(props.meta.data_through) < Math.min(Date.parse(props.meta.range.to), Date.now()) - 300_000)
</script>