perf(dashboard): derive stats from the cached config

The dashboard fired eight requests on every visit although seven of them (management keys, per-provider key lists, ampcode) are already present on the normalized config it subscribes to. Keep only the auth-files fetch and read everything else from the store; fetchConfig() is deduped and TTL-cached.
This commit is contained in:
LTbinglingfeng
2026-06-13 02:24:00 +08:00
Unverified
parent a4fcd76998
commit c79f58a4ac
+59 -112
View File
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IconKey, IconBot, IconFileText, IconSatellite } from '@/components/ui/icons'; import { IconKey, IconBot, IconFileText, IconSatellite } from '@/components/ui/icons';
import { useAuthStore, useConfigStore, useModelsStore } from '@/stores'; import { useAuthStore, useConfigStore, useModelsStore } from '@/stores';
import { apiKeysApi, providersApi, authFilesApi, ampcodeApi } from '@/services/api'; import { authFilesApi } from '@/services/api';
import { useApiKeysForModels } from '@/hooks/useApiKeysForModels'; import { useApiKeysForModels } from '@/hooks/useApiKeysForModels';
import type { AmpcodeConfig } from '@/types'; import type { AmpcodeConfig } from '@/types';
import { formatDateValue } from '@/utils/format'; import { formatDateValue } from '@/utils/format';
@@ -18,25 +18,17 @@ interface QuickStat {
sublabel?: string; sublabel?: string;
} }
interface ProviderStats {
gemini: number | null;
codex: number | null;
claude: number | null;
vertex: number | null;
openai: number | null;
ampcode: number | null;
}
type TimeOfDay = 'morning' | 'afternoon' | 'evening' | 'night'; type TimeOfDay = 'morning' | 'afternoon' | 'evening' | 'night';
const countAmpcodeConfig = (value: AmpcodeConfig | undefined): number => { const countAmpcodeConfig = (value: AmpcodeConfig | undefined): number => {
if (!value) return 0; if (!value) return 0;
if (value.upstreamUrl?.trim()) return 1; const configured =
if (value.upstreamApiKey?.trim()) return 1; Boolean(value.upstreamUrl?.trim()) ||
if ((value.upstreamApiKeys?.length ?? 0) > 0) return 1; Boolean(value.upstreamApiKey?.trim()) ||
if ((value.modelMappings?.length ?? 0) > 0) return 1; (value.upstreamApiKeys?.length ?? 0) > 0 ||
if (value.forceModelMappings === true) return 1; (value.modelMappings?.length ?? 0) > 0 ||
return 0; value.forceModelMappings === true;
return configured ? 1 : 0;
}; };
function getTimeOfDay(): TimeOfDay { function getTimeOfDay(): TimeOfDay {
@@ -54,29 +46,14 @@ export function DashboardPage() {
const serverBuildDate = useAuthStore((state) => state.serverBuildDate); const serverBuildDate = useAuthStore((state) => state.serverBuildDate);
const apiBase = useAuthStore((state) => state.apiBase); const apiBase = useAuthStore((state) => state.apiBase);
const config = useConfigStore((state) => state.config); const config = useConfigStore((state) => state.config);
const fetchConfig = useConfigStore((state) => state.fetchConfig);
const models = useModelsStore((state) => state.models); const models = useModelsStore((state) => state.models);
const modelsLoading = useModelsStore((state) => state.loading); const modelsLoading = useModelsStore((state) => state.loading);
const fetchModelsFromStore = useModelsStore((state) => state.fetchModels); const fetchModelsFromStore = useModelsStore((state) => state.fetchModels);
const [stats, setStats] = useState<{ const [authFilesCount, setAuthFilesCount] = useState<number | null>(null);
apiKeys: number | null; const [authFilesLoading, setAuthFilesLoading] = useState(false);
authFiles: number | null;
}>({
apiKeys: null,
authFiles: null,
});
const [providerStats, setProviderStats] = useState<ProviderStats>({
gemini: null,
codex: null,
claude: null,
vertex: null,
openai: null,
ampcode: null,
});
const [loading, setLoading] = useState(true);
// Time-of-day state for dynamic greeting // Time-of-day state for dynamic greeting
const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>(getTimeOfDay); const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>(getTimeOfDay);
@@ -108,111 +85,81 @@ export function DashboardPage() {
}, [connectionStatus, apiBase, resolveApiKeysForModels, fetchModelsFromStore]); }, [connectionStatus, apiBase, resolveApiKeysForModels, fetchModelsFromStore]);
useEffect(() => { useEffect(() => {
const fetchStats = async () => { if (connectionStatus !== 'connected') {
setLoading(true); return;
}
let cancelled = false;
const loadAuthFiles = async () => {
setAuthFilesLoading(true);
try { try {
const [ const res = await authFilesApi.list();
keysRes, if (!cancelled) setAuthFilesCount(res.files.length);
filesRes, } catch {
geminiRes, if (!cancelled) setAuthFilesCount(null);
codexRes,
claudeRes,
vertexRes,
openaiRes,
ampcodeRes,
] = await Promise.allSettled([
apiKeysApi.list(),
authFilesApi.list(),
providersApi.getGeminiKeys(),
providersApi.getCodexConfigs(),
providersApi.getClaudeConfigs(),
providersApi.getVertexConfigs(),
providersApi.getOpenAIProviders(),
ampcodeApi.getAmpcode(),
]);
setStats({
apiKeys: keysRes.status === 'fulfilled' ? keysRes.value.length : null,
authFiles: filesRes.status === 'fulfilled' ? filesRes.value.files.length : null,
});
setProviderStats({
gemini: geminiRes.status === 'fulfilled' ? geminiRes.value.length : null,
codex: codexRes.status === 'fulfilled' ? codexRes.value.length : null,
claude: claudeRes.status === 'fulfilled' ? claudeRes.value.length : null,
vertex: vertexRes.status === 'fulfilled' ? vertexRes.value.length : null,
openai: openaiRes.status === 'fulfilled' ? openaiRes.value.length : null,
ampcode: ampcodeRes.status === 'fulfilled' ? countAmpcodeConfig(ampcodeRes.value) : null,
});
} finally { } finally {
setLoading(false); setAuthFilesLoading(false);
} }
}; };
if (connectionStatus === 'connected') { // 提供商/密钥统计直接来自 config store;这里只需保证配置已加载并取认证文件数。
fetchStats(); fetchConfig().catch(() => undefined);
fetchModels(); fetchModels();
} else { void loadAuthFiles();
setLoading(false);
}
}, [connectionStatus, fetchModels]);
// Calculate total provider keys only when all provider stats are available. return () => {
const providerStatsReady = cancelled = true;
providerStats.gemini !== null && };
providerStats.codex !== null && }, [connectionStatus, fetchConfig, fetchModels]);
providerStats.claude !== null &&
providerStats.vertex !== null && const configLoading = !config;
providerStats.openai !== null && const providerStats = config
providerStats.ampcode !== null; ? {
const hasProviderStats = gemini: config.geminiApiKeys?.length ?? 0,
providerStats.gemini !== null || codex: config.codexApiKeys?.length ?? 0,
providerStats.codex !== null || claude: config.claudeApiKeys?.length ?? 0,
providerStats.claude !== null || vertex: config.vertexApiKeys?.length ?? 0,
providerStats.vertex !== null || openai: config.openaiCompatibility?.length ?? 0,
providerStats.openai !== null || ampcode: countAmpcodeConfig(config.ampcode),
providerStats.ampcode !== null; }
const totalProviderKeys = providerStatsReady : null;
? (providerStats.gemini ?? 0) + const totalProviderKeys = providerStats
(providerStats.codex ?? 0) + ? Object.values(providerStats).reduce((sum, count) => sum + count, 0)
(providerStats.claude ?? 0) +
(providerStats.vertex ?? 0) +
(providerStats.openai ?? 0) +
(providerStats.ampcode ?? 0)
: 0; : 0;
const quickStats: QuickStat[] = [ const quickStats: QuickStat[] = [
{ {
label: t('dashboard.management_keys'), label: t('dashboard.management_keys'),
value: stats.apiKeys ?? '-', value: config ? (config.apiKeys?.length ?? 0) : '-',
icon: <IconKey size={24} />, icon: <IconKey size={24} />,
path: '/config', path: '/config',
loading: loading && stats.apiKeys === null, loading: configLoading,
sublabel: t('nav.config_management'), sublabel: t('nav.config_management'),
}, },
{ {
label: t('nav.ai_providers'), label: t('nav.ai_providers'),
value: loading ? '-' : providerStatsReady ? totalProviderKeys : '-', value: providerStats ? totalProviderKeys : '-',
icon: <IconBot size={24} />, icon: <IconBot size={24} />,
path: '/ai-providers', path: '/ai-providers',
loading: loading, loading: configLoading,
sublabel: hasProviderStats sublabel: providerStats
? t('dashboard.provider_keys_detail', { ? t('dashboard.provider_keys_detail', {
gemini: providerStats.gemini ?? '-', gemini: providerStats.gemini,
codex: providerStats.codex ?? '-', codex: providerStats.codex,
claude: providerStats.claude ?? '-', claude: providerStats.claude,
vertex: providerStats.vertex ?? '-', vertex: providerStats.vertex,
openai: providerStats.openai ?? '-', openai: providerStats.openai,
ampcode: providerStats.ampcode ?? '-', ampcode: providerStats.ampcode,
}) })
: undefined, : undefined,
}, },
{ {
label: t('nav.auth_files'), label: t('nav.auth_files'),
value: stats.authFiles ?? '-', value: authFilesCount ?? '-',
icon: <IconFileText size={24} />, icon: <IconFileText size={24} />,
path: '/auth-files', path: '/auth-files',
loading: loading && stats.authFiles === null, loading: authFilesLoading && authFilesCount === null,
sublabel: t('dashboard.oauth_credentials'), sublabel: t('dashboard.oauth_credentials'),
}, },
{ {