feat: 实现基于滑动窗口的实时RPM/TPM统计

- 添加系统级分钟统计,支持1-60分钟可配置时间窗口
- 新增 getRealtimeSystemMetrics 方法计算滑动窗口内的平均值
- 前端显示实时RPM/TPM,标注时间窗口和数据来源
- 修复 EditApiKeyModal 中模型限制和客户端限制复选框状态错误
- 优化性能:使用Pipeline批量操作替代Promise.all
- TPM包含所有token类型:input、output、cache_creation、cache_read
- 添加降级方案:实时数据不可用时返回历史平均值

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-30 14:27:34 +08:00
parent 363d1c3ed3
commit a6ab6b7abe
7 changed files with 237 additions and 103 deletions

View File

@@ -45,6 +45,7 @@ TOKEN_USAGE_RETENTION=2592000000
HEALTH_CHECK_INTERVAL=60000
SYSTEM_TIMEZONE=Asia/Shanghai
TIMEZONE_OFFSET=8
METRICS_WINDOW=5 # 实时指标统计窗口分钟可选1-60默认5分钟
# 🎨 Web 界面配置
WEB_TITLE=Claude Relay Service

View File

@@ -186,6 +186,10 @@ class RedisClient {
const keyModelMonthly = `usage:${keyId}:model:monthly:${model}:${currentMonth}`;
const keyModelHourly = `usage:${keyId}:model:hourly:${model}:${currentHour}`; // 新增API Key模型小时级别
// 新增:系统级分钟统计
const minuteTimestamp = Math.floor(now.getTime() / 60000);
const systemMinuteKey = `system:metrics:minute:${minuteTimestamp}`;
// 智能处理输入输出token分配
const finalInputTokens = inputTokens || 0;
const finalOutputTokens = outputTokens || (finalInputTokens > 0 ? 0 : tokens);
@@ -197,96 +201,122 @@ class RedisClient {
// 核心token不包括缓存- 用于与历史数据兼容
const coreTokens = finalInputTokens + finalOutputTokens;
await Promise.all([
// 核心token统计保持向后兼容
this.client.hincrby(key, 'totalTokens', coreTokens),
this.client.hincrby(key, 'totalInputTokens', finalInputTokens),
this.client.hincrby(key, 'totalOutputTokens', finalOutputTokens),
// 缓存token统计新增
this.client.hincrby(key, 'totalCacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(key, 'totalCacheReadTokens', finalCacheReadTokens),
this.client.hincrby(key, 'totalAllTokens', totalTokens), // 包含所有类型的总token
// 请求计数
this.client.hincrby(key, 'totalRequests', 1),
// 每日统计
this.client.hincrby(daily, 'tokens', coreTokens),
this.client.hincrby(daily, 'inputTokens', finalInputTokens),
this.client.hincrby(daily, 'outputTokens', finalOutputTokens),
this.client.hincrby(daily, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(daily, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(daily, 'allTokens', totalTokens),
this.client.hincrby(daily, 'requests', 1),
// 每月统计
this.client.hincrby(monthly, 'tokens', coreTokens),
this.client.hincrby(monthly, 'inputTokens', finalInputTokens),
this.client.hincrby(monthly, 'outputTokens', finalOutputTokens),
this.client.hincrby(monthly, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(monthly, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(monthly, 'allTokens', totalTokens),
this.client.hincrby(monthly, 'requests', 1),
// 按模型统计 - 每日
this.client.hincrby(modelDaily, 'inputTokens', finalInputTokens),
this.client.hincrby(modelDaily, 'outputTokens', finalOutputTokens),
this.client.hincrby(modelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(modelDaily, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(modelDaily, 'allTokens', totalTokens),
this.client.hincrby(modelDaily, 'requests', 1),
// 按模型统计 - 每月
this.client.hincrby(modelMonthly, 'inputTokens', finalInputTokens),
this.client.hincrby(modelMonthly, 'outputTokens', finalOutputTokens),
this.client.hincrby(modelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(modelMonthly, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(modelMonthly, 'allTokens', totalTokens),
this.client.hincrby(modelMonthly, 'requests', 1),
// API Key级别的模型统计 - 每日
this.client.hincrby(keyModelDaily, 'inputTokens', finalInputTokens),
this.client.hincrby(keyModelDaily, 'outputTokens', finalOutputTokens),
this.client.hincrby(keyModelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(keyModelDaily, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(keyModelDaily, 'allTokens', totalTokens),
this.client.hincrby(keyModelDaily, 'requests', 1),
// API Key级别的模型统计 - 每月
this.client.hincrby(keyModelMonthly, 'inputTokens', finalInputTokens),
this.client.hincrby(keyModelMonthly, 'outputTokens', finalOutputTokens),
this.client.hincrby(keyModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(keyModelMonthly, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(keyModelMonthly, 'allTokens', totalTokens),
this.client.hincrby(keyModelMonthly, 'requests', 1),
// 使用Pipeline优化性能
const pipeline = this.client.pipeline();
// 小时级别统计
this.client.hincrby(hourly, 'tokens', coreTokens),
this.client.hincrby(hourly, 'inputTokens', finalInputTokens),
this.client.hincrby(hourly, 'outputTokens', finalOutputTokens),
this.client.hincrby(hourly, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(hourly, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(hourly, 'allTokens', totalTokens),
this.client.hincrby(hourly, 'requests', 1),
// 按模型统计 - 每小时
this.client.hincrby(modelHourly, 'inputTokens', finalInputTokens),
this.client.hincrby(modelHourly, 'outputTokens', finalOutputTokens),
this.client.hincrby(modelHourly, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(modelHourly, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(modelHourly, 'allTokens', totalTokens),
this.client.hincrby(modelHourly, 'requests', 1),
// API Key级别的模型统计 - 每小时
this.client.hincrby(keyModelHourly, 'inputTokens', finalInputTokens),
this.client.hincrby(keyModelHourly, 'outputTokens', finalOutputTokens),
this.client.hincrby(keyModelHourly, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(keyModelHourly, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(keyModelHourly, 'allTokens', totalTokens),
this.client.hincrby(keyModelHourly, 'requests', 1),
// 现有的统计保持不变
// 核心token统计保持向后兼容
pipeline.hincrby(key, 'totalTokens', coreTokens);
pipeline.hincrby(key, 'totalInputTokens', finalInputTokens);
pipeline.hincrby(key, 'totalOutputTokens', finalOutputTokens);
// 缓存token统计新增
pipeline.hincrby(key, 'totalCacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(key, 'totalCacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(key, 'totalAllTokens', totalTokens); // 包含所有类型的总token
// 请求计数
pipeline.hincrby(key, 'totalRequests', 1);
// 设置过期时间
this.client.expire(daily, 86400 * 32), // 32天过期
this.client.expire(monthly, 86400 * 365), // 1年过期
this.client.expire(hourly, 86400 * 7), // 小时统计7天过期
this.client.expire(modelDaily, 86400 * 32), // 模型每日统计32天过期
this.client.expire(modelMonthly, 86400 * 365), // 模型每月统计1年过期
this.client.expire(modelHourly, 86400 * 7), // 模型小时统计7天过期
this.client.expire(keyModelDaily, 86400 * 32), // API Key模型每日统计32天过期
this.client.expire(keyModelMonthly, 86400 * 365), // API Key模型每月统计1年过期
this.client.expire(keyModelHourly, 86400 * 7) // API Key模型小时统计7天过期
]);
// 每日统计
pipeline.hincrby(daily, 'tokens', coreTokens);
pipeline.hincrby(daily, 'inputTokens', finalInputTokens);
pipeline.hincrby(daily, 'outputTokens', finalOutputTokens);
pipeline.hincrby(daily, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(daily, 'cacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(daily, 'allTokens', totalTokens);
pipeline.hincrby(daily, 'requests', 1);
// 每月统计
pipeline.hincrby(monthly, 'tokens', coreTokens);
pipeline.hincrby(monthly, 'inputTokens', finalInputTokens);
pipeline.hincrby(monthly, 'outputTokens', finalOutputTokens);
pipeline.hincrby(monthly, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(monthly, 'cacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(monthly, 'allTokens', totalTokens);
pipeline.hincrby(monthly, 'requests', 1);
// 按模型统计 - 每日
pipeline.hincrby(modelDaily, 'inputTokens', finalInputTokens);
pipeline.hincrby(modelDaily, 'outputTokens', finalOutputTokens);
pipeline.hincrby(modelDaily, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(modelDaily, 'cacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(modelDaily, 'allTokens', totalTokens);
pipeline.hincrby(modelDaily, 'requests', 1);
// 按模型统计 - 每月
pipeline.hincrby(modelMonthly, 'inputTokens', finalInputTokens);
pipeline.hincrby(modelMonthly, 'outputTokens', finalOutputTokens);
pipeline.hincrby(modelMonthly, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(modelMonthly, 'cacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(modelMonthly, 'allTokens', totalTokens);
pipeline.hincrby(modelMonthly, 'requests', 1);
// API Key级别的模型统计 - 每日
pipeline.hincrby(keyModelDaily, 'inputTokens', finalInputTokens);
pipeline.hincrby(keyModelDaily, 'outputTokens', finalOutputTokens);
pipeline.hincrby(keyModelDaily, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(keyModelDaily, 'cacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(keyModelDaily, 'allTokens', totalTokens);
pipeline.hincrby(keyModelDaily, 'requests', 1);
// API Key级别的模型统计 - 每月
pipeline.hincrby(keyModelMonthly, 'inputTokens', finalInputTokens);
pipeline.hincrby(keyModelMonthly, 'outputTokens', finalOutputTokens);
pipeline.hincrby(keyModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(keyModelMonthly, 'cacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(keyModelMonthly, 'allTokens', totalTokens);
pipeline.hincrby(keyModelMonthly, 'requests', 1);
// 小时级别统计
pipeline.hincrby(hourly, 'tokens', coreTokens);
pipeline.hincrby(hourly, 'inputTokens', finalInputTokens);
pipeline.hincrby(hourly, 'outputTokens', finalOutputTokens);
pipeline.hincrby(hourly, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(hourly, 'cacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(hourly, 'allTokens', totalTokens);
pipeline.hincrby(hourly, 'requests', 1);
// 按模型统计 - 每小时
pipeline.hincrby(modelHourly, 'inputTokens', finalInputTokens);
pipeline.hincrby(modelHourly, 'outputTokens', finalOutputTokens);
pipeline.hincrby(modelHourly, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(modelHourly, 'cacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(modelHourly, 'allTokens', totalTokens);
pipeline.hincrby(modelHourly, 'requests', 1);
// API Key级别的模型统计 - 每小时
pipeline.hincrby(keyModelHourly, 'inputTokens', finalInputTokens);
pipeline.hincrby(keyModelHourly, 'outputTokens', finalOutputTokens);
pipeline.hincrby(keyModelHourly, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(keyModelHourly, 'cacheReadTokens', finalCacheReadTokens);
pipeline.hincrby(keyModelHourly, 'allTokens', totalTokens);
pipeline.hincrby(keyModelHourly, 'requests', 1);
// 新增:系统级分钟统计
pipeline.hincrby(systemMinuteKey, 'requests', 1);
pipeline.hincrby(systemMinuteKey, 'totalTokens', totalTokens);
pipeline.hincrby(systemMinuteKey, 'inputTokens', finalInputTokens);
pipeline.hincrby(systemMinuteKey, 'outputTokens', finalOutputTokens);
pipeline.hincrby(systemMinuteKey, 'cacheCreateTokens', finalCacheCreateTokens);
pipeline.hincrby(systemMinuteKey, 'cacheReadTokens', finalCacheReadTokens);
// 设置过期时间
pipeline.expire(daily, 86400 * 32); // 32天过期
pipeline.expire(monthly, 86400 * 365); // 1年过期
pipeline.expire(hourly, 86400 * 7); // 小时统计7天过期
pipeline.expire(modelDaily, 86400 * 32); // 模型每日统计32天过期
pipeline.expire(modelMonthly, 86400 * 365); // 模型每月统计1年过期
pipeline.expire(modelHourly, 86400 * 7); // 模型小时统计7天过期
pipeline.expire(keyModelDaily, 86400 * 32); // API Key模型每日统计32天过期
pipeline.expire(keyModelMonthly, 86400 * 365); // API Key模型每月统计1年过期
pipeline.expire(keyModelHourly, 86400 * 7); // API Key模型小时统计7天过期
// 系统级分钟统计的过期时间窗口时间的2倍
const config = require('../../config/config');
const metricsWindow = config.system.metricsWindow;
pipeline.expire(systemMinuteKey, metricsWindow * 60 * 2);
// 执行Pipeline
await pipeline.exec();
}
// 📊 记录账户级别的使用统计
@@ -974,6 +1004,76 @@ class RedisClient {
}
}
// 📊 获取实时系统指标(基于滑动窗口)
async getRealtimeSystemMetrics() {
try {
const config = require('../../config/config');
const windowMinutes = config.system.metricsWindow;
const now = new Date();
const currentMinute = Math.floor(now.getTime() / 60000);
// 使用Pipeline批量获取窗口内的所有分钟数据
const pipeline = this.client.pipeline();
for (let i = 0; i < windowMinutes; i++) {
const minuteKey = `system:metrics:minute:${currentMinute - i}`;
pipeline.hgetall(minuteKey);
}
const results = await pipeline.exec();
// 聚合计算
let totalRequests = 0;
let totalTokens = 0;
let totalInputTokens = 0;
let totalOutputTokens = 0;
let totalCacheCreateTokens = 0;
let totalCacheReadTokens = 0;
results.forEach(([err, data]) => {
if (!err && data) {
totalRequests += parseInt(data.requests || 0);
totalTokens += parseInt(data.totalTokens || 0);
totalInputTokens += parseInt(data.inputTokens || 0);
totalOutputTokens += parseInt(data.outputTokens || 0);
totalCacheCreateTokens += parseInt(data.cacheCreateTokens || 0);
totalCacheReadTokens += parseInt(data.cacheReadTokens || 0);
}
});
// 计算平均值(每分钟)
const realtimeRPM = windowMinutes > 0 ? Math.round((totalRequests / windowMinutes) * 100) / 100 : 0;
const realtimeTPM = windowMinutes > 0 ? Math.round((totalTokens / windowMinutes) * 100) / 100 : 0;
return {
realtimeRPM,
realtimeTPM,
windowMinutes,
totalRequests,
totalTokens,
totalInputTokens,
totalOutputTokens,
totalCacheCreateTokens,
totalCacheReadTokens
};
} catch (error) {
console.error('Error getting realtime system metrics:', error);
// 如果出错,返回历史平均值作为降级方案
const historicalMetrics = await this.getSystemAverages();
return {
realtimeRPM: historicalMetrics.systemRPM,
realtimeTPM: historicalMetrics.systemTPM,
windowMinutes: 0, // 标识使用了历史数据
totalRequests: 0,
totalTokens: historicalMetrics.totalTokens,
totalInputTokens: historicalMetrics.totalInputTokens,
totalOutputTokens: historicalMetrics.totalOutputTokens,
totalCacheCreateTokens: 0,
totalCacheReadTokens: 0
};
}
}
// 🔗 会话sticky映射管理
async setSessionAccountMapping(sessionHash, accountId, ttl = 3600) {
const key = `sticky_session:${sessionHash}`;

View File

@@ -1261,13 +1261,14 @@ router.get('/accounts/:accountId/usage-stats', authenticateAdmin, async (req, re
// 获取系统概览
router.get('/dashboard', authenticateAdmin, async (req, res) => {
try {
const [, apiKeys, claudeAccounts, geminiAccounts, todayStats, systemAverages] = await Promise.all([
const [, apiKeys, claudeAccounts, geminiAccounts, todayStats, systemAverages, realtimeMetrics] = await Promise.all([
redis.getSystemStats(),
apiKeyService.getAllApiKeys(),
claudeAccountService.getAllAccounts(),
geminiAccountService.getAllAccounts(),
redis.getTodayStats(),
redis.getSystemAverages()
redis.getSystemAverages(),
redis.getRealtimeSystemMetrics()
]);
// 计算使用统计统一使用allTokens
@@ -1316,6 +1317,12 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
rpm: systemAverages.systemRPM,
tpm: systemAverages.systemTPM
},
realtimeMetrics: {
rpm: realtimeMetrics.realtimeRPM,
tpm: realtimeMetrics.realtimeTPM,
windowMinutes: realtimeMetrics.windowMinutes,
isHistorical: realtimeMetrics.windowMinutes === 0 // 标识是否使用了历史数据
},
systemHealth: {
redisConnected: redis.isConnected,
claudeAccountsHealthy: activeClaudeAccounts > 0,
@@ -1483,7 +1490,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
endTime = new Date(endDate);
// 调试日志
logger.info(`📊 Usage trend hour granularity - received times:`);
logger.info('📊 Usage trend hour granularity - received times:');
logger.info(` startDate (raw): ${startDate}`);
logger.info(` endDate (raw): ${endDate}`);
logger.info(` startTime (parsed): ${startTime.toISOString()}`);

View File

@@ -88,11 +88,11 @@ class ClaudeConsoleRelayService {
logger.debug(`[DEBUG] Adding beta header: ${options.betaHeader}`);
requestConfig.headers['anthropic-beta'] = options.betaHeader;
} else {
logger.debug(`[DEBUG] No beta header to add`);
logger.debug('[DEBUG] No beta header to add');
}
// 发送请求
logger.debug(`📤 Sending request to Claude Console API with headers:`, JSON.stringify(requestConfig.headers, null, 2));
logger.debug('📤 Sending request to Claude Console API with headers:', JSON.stringify(requestConfig.headers, null, 2));
const response = await axios(requestConfig);
// 移除监听器(请求成功完成)

View File

@@ -522,8 +522,9 @@ onMounted(async () => {
form.restrictedModels = props.apiKey.restrictedModels || []
form.allowedClients = props.apiKey.allowedClients || []
form.tags = props.apiKey.tags || []
form.enableModelRestriction = form.restrictedModels.length > 0
form.enableClientRestriction = form.allowedClients.length > 0
// 从后端数据中获取实际的启用状态,而不是根据数组长度推断
form.enableModelRestriction = props.apiKey.enableModelRestriction || false
form.enableClientRestriction = props.apiKey.enableClientRestriction || false
})
</script>

View File

@@ -26,6 +26,10 @@ export const useDashboardStore = defineStore('dashboard', () => {
todayCacheReadTokens: 0,
systemRPM: 0,
systemTPM: 0,
realtimeRPM: 0,
realtimeTPM: 0,
metricsWindow: 5,
isHistoricalMetrics: false,
systemStatus: '正常',
uptime: 0,
systemTimezone: 8 // 默认 UTC+8
@@ -129,6 +133,7 @@ export const useDashboardStore = defineStore('dashboard', () => {
const overview = dashboardResponse.data.overview || {}
const recentActivity = dashboardResponse.data.recentActivity || {}
const systemAverages = dashboardResponse.data.systemAverages || {}
const realtimeMetrics = dashboardResponse.data.realtimeMetrics || {}
const systemHealth = dashboardResponse.data.systemHealth || {}
dashboardData.value = {
@@ -151,6 +156,10 @@ export const useDashboardStore = defineStore('dashboard', () => {
todayCacheReadTokens: recentActivity.cacheReadTokensToday || 0,
systemRPM: systemAverages.rpm || 0,
systemTPM: systemAverages.tpm || 0,
realtimeRPM: realtimeMetrics.rpm || 0,
realtimeTPM: realtimeMetrics.tpm || 0,
metricsWindow: realtimeMetrics.windowMinutes || 5,
isHistoricalMetrics: realtimeMetrics.isHistorical || false,
systemStatus: systemHealth.redisConnected ? '正常' : '异常',
uptime: systemHealth.uptime || 0,
systemTimezone: dashboardResponse.data.systemTimezone || 8

View File

@@ -111,9 +111,17 @@
<div class="stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-600 mb-1">平均RPM</p>
<p class="text-3xl font-bold text-orange-600">{{ dashboardData.systemRPM || 0 }}</p>
<p class="text-xs text-gray-500 mt-1">分钟请求数</p>
<p class="text-sm font-semibold text-gray-600 mb-1">
实时RPM
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
</p>
<p class="text-3xl font-bold text-orange-600">{{ dashboardData.realtimeRPM || 0 }}</p>
<p class="text-xs text-gray-500 mt-1">
每分钟请求数
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600">
<i class="fas fa-exclamation-circle"></i> 历史数据
</span>
</p>
</div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-orange-500 to-orange-600">
<i class="fas fa-tachometer-alt"></i>
@@ -124,9 +132,17 @@
<div class="stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-600 mb-1">平均TPM</p>
<p class="text-3xl font-bold text-rose-600">{{ dashboardData.systemTPM || 0 }}</p>
<p class="text-xs text-gray-500 mt-1">分钟Token数</p>
<p class="text-sm font-semibold text-gray-600 mb-1">
实时TPM
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
</p>
<p class="text-3xl font-bold text-rose-600">{{ dashboardData.realtimeTPM || 0 }}</p>
<p class="text-xs text-gray-500 mt-1">
每分钟Token数
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600">
<i class="fas fa-exclamation-circle"></i> 历史数据
</span>
</p>
</div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-rose-500 to-rose-600">
<i class="fas fa-rocket"></i>