mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
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:
+59
-112
@@ -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'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user