mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 18:41:35 +08:00
feat: add per-app usage filtering (Claude/Codex/Gemini)
Add dashboard-level app type filter to usage statistics, replacing the DataSourceBar with a more useful segmented control. All components (summary cards, trend chart, provider stats, model stats, request logs) now respond to the selected app filter. Backend: add optional app_type parameter to get_usage_summary, get_daily_trends, get_provider_stats, and get_model_stats queries. Frontend: new AppTypeFilter type, updated query keys with appType dimension for proper cache separation, and RequestLogTable local filter auto-locks when dashboard filter is active.
This commit is contained in:
@@ -11,8 +11,11 @@ pub fn get_usage_summary(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
start_date: Option<i64>,
|
start_date: Option<i64>,
|
||||||
end_date: Option<i64>,
|
end_date: Option<i64>,
|
||||||
|
app_type: Option<String>,
|
||||||
) -> Result<UsageSummary, AppError> {
|
) -> Result<UsageSummary, AppError> {
|
||||||
state.db.get_usage_summary(start_date, end_date)
|
state
|
||||||
|
.db
|
||||||
|
.get_usage_summary(start_date, end_date, app_type.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取每日趋势
|
/// 获取每日趋势
|
||||||
@@ -21,20 +24,29 @@ pub fn get_usage_trends(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
start_date: Option<i64>,
|
start_date: Option<i64>,
|
||||||
end_date: Option<i64>,
|
end_date: Option<i64>,
|
||||||
|
app_type: Option<String>,
|
||||||
) -> Result<Vec<DailyStats>, AppError> {
|
) -> Result<Vec<DailyStats>, AppError> {
|
||||||
state.db.get_daily_trends(start_date, end_date)
|
state
|
||||||
|
.db
|
||||||
|
.get_daily_trends(start_date, end_date, app_type.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 Provider 统计
|
/// 获取 Provider 统计
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_provider_stats(state: State<'_, AppState>) -> Result<Vec<ProviderStats>, AppError> {
|
pub fn get_provider_stats(
|
||||||
state.db.get_provider_stats()
|
state: State<'_, AppState>,
|
||||||
|
app_type: Option<String>,
|
||||||
|
) -> Result<Vec<ProviderStats>, AppError> {
|
||||||
|
state.db.get_provider_stats(app_type.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取模型统计
|
/// 获取模型统计
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_model_stats(state: State<'_, AppState>) -> Result<Vec<ModelStats>, AppError> {
|
pub fn get_model_stats(
|
||||||
state.db.get_model_stats()
|
state: State<'_, AppState>,
|
||||||
|
app_type: Option<String>,
|
||||||
|
) -> Result<Vec<ModelStats>, AppError> {
|
||||||
|
state.db.get_model_stats(app_type.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取请求日志列表
|
/// 获取请求日志列表
|
||||||
|
|||||||
@@ -123,44 +123,54 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
start_date: Option<i64>,
|
start_date: Option<i64>,
|
||||||
end_date: Option<i64>,
|
end_date: Option<i64>,
|
||||||
|
app_type: Option<&str>,
|
||||||
) -> Result<UsageSummary, AppError> {
|
) -> Result<UsageSummary, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
let (where_clause, params_vec) = if start_date.is_some() || end_date.is_some() {
|
// Build detail WHERE clause
|
||||||
let mut conditions = Vec::new();
|
let mut conditions = Vec::new();
|
||||||
let mut params = Vec::new();
|
let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||||
|
|
||||||
if let Some(start) = start_date {
|
if let Some(start) = start_date {
|
||||||
conditions.push("created_at >= ?");
|
conditions.push("created_at >= ?");
|
||||||
params.push(start);
|
params_vec.push(Box::new(start));
|
||||||
}
|
}
|
||||||
if let Some(end) = end_date {
|
if let Some(end) = end_date {
|
||||||
conditions.push("created_at <= ?");
|
conditions.push("created_at <= ?");
|
||||||
params.push(end);
|
params_vec.push(Box::new(end));
|
||||||
}
|
}
|
||||||
|
if let Some(at) = app_type {
|
||||||
|
conditions.push("app_type = ?");
|
||||||
|
params_vec.push(Box::new(at.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
(format!("WHERE {}", conditions.join(" AND ")), params)
|
let where_clause = if conditions.is_empty() {
|
||||||
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
(String::new(), Vec::new())
|
format!("WHERE {}", conditions.join(" AND "))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build rollup WHERE clause using date strings (use ? for sequential binding)
|
// Build rollup WHERE clause using date strings
|
||||||
let (rollup_where, rollup_params) = if start_date.is_some() || end_date.is_some() {
|
let mut rollup_conditions: Vec<String> = Vec::new();
|
||||||
let mut conditions: Vec<String> = Vec::new();
|
let mut rollup_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||||
let mut params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(start) = start_date {
|
if let Some(start) = start_date {
|
||||||
conditions.push("date >= date(?, 'unixepoch', 'localtime')".to_string());
|
rollup_conditions.push("date >= date(?, 'unixepoch', 'localtime')".to_string());
|
||||||
params.push(start);
|
rollup_params.push(Box::new(start));
|
||||||
}
|
}
|
||||||
if let Some(end) = end_date {
|
if let Some(end) = end_date {
|
||||||
conditions.push("date <= date(?, 'unixepoch', 'localtime')".to_string());
|
rollup_conditions.push("date <= date(?, 'unixepoch', 'localtime')".to_string());
|
||||||
params.push(end);
|
rollup_params.push(Box::new(end));
|
||||||
}
|
}
|
||||||
|
if let Some(at) = app_type {
|
||||||
|
rollup_conditions.push("app_type = ?".to_string());
|
||||||
|
rollup_params.push(Box::new(at.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
(format!("WHERE {}", conditions.join(" AND ")), params)
|
let rollup_where = if rollup_conditions.is_empty() {
|
||||||
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
(String::new(), Vec::new())
|
format!("WHERE {}", rollup_conditions.join(" AND "))
|
||||||
};
|
};
|
||||||
|
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
@@ -194,10 +204,11 @@ impl Database {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Combine params: detail params first, then rollup params
|
// Combine params: detail params first, then rollup params
|
||||||
let mut all_params: Vec<i64> = params_vec;
|
let mut all_params: Vec<Box<dyn rusqlite::ToSql>> = params_vec;
|
||||||
all_params.extend(rollup_params);
|
all_params.extend(rollup_params);
|
||||||
|
let param_refs: Vec<&dyn rusqlite::ToSql> = all_params.iter().map(|p| p.as_ref()).collect();
|
||||||
|
|
||||||
let result = conn.query_row(&sql, rusqlite::params_from_iter(all_params), |row| {
|
let result = conn.query_row(&sql, param_refs.as_slice(), |row| {
|
||||||
let total_requests: i64 = row.get(0)?;
|
let total_requests: i64 = row.get(0)?;
|
||||||
let total_cost: f64 = row.get(1)?;
|
let total_cost: f64 = row.get(1)?;
|
||||||
let total_input_tokens: i64 = row.get(2)?;
|
let total_input_tokens: i64 = row.get(2)?;
|
||||||
@@ -231,6 +242,7 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
start_date: Option<i64>,
|
start_date: Option<i64>,
|
||||||
end_date: Option<i64>,
|
end_date: Option<i64>,
|
||||||
|
app_type: Option<&str>,
|
||||||
) -> Result<Vec<DailyStats>, AppError> {
|
) -> Result<Vec<DailyStats>, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
@@ -262,9 +274,15 @@ impl Database {
|
|||||||
bucket_count = 1;
|
bucket_count = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let app_type_filter = if app_type.is_some() {
|
||||||
|
"AND app_type = ?4"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
// Query detail logs
|
// Query detail logs
|
||||||
let sql = "
|
let sql = format!(
|
||||||
SELECT
|
"SELECT
|
||||||
CAST((created_at - ?1) / ?3 AS INTEGER) as bucket_idx,
|
CAST((created_at - ?1) / ?3 AS INTEGER) as bucket_idx,
|
||||||
COUNT(*) as request_count,
|
COUNT(*) as request_count,
|
||||||
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost,
|
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost,
|
||||||
@@ -274,12 +292,13 @@ impl Database {
|
|||||||
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
|
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
|
||||||
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens
|
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens
|
||||||
FROM proxy_request_logs
|
FROM proxy_request_logs
|
||||||
WHERE created_at >= ?1 AND created_at <= ?2
|
WHERE created_at >= ?1 AND created_at <= ?2 {app_type_filter}
|
||||||
GROUP BY bucket_idx
|
GROUP BY bucket_idx
|
||||||
ORDER BY bucket_idx ASC";
|
ORDER BY bucket_idx ASC"
|
||||||
|
);
|
||||||
|
|
||||||
let mut stmt = conn.prepare(sql)?;
|
let mut stmt = conn.prepare(&sql)?;
|
||||||
let rows = stmt.query_map(params![start_ts, end_ts, bucket_seconds], |row| {
|
let row_mapper = |row: &rusqlite::Row| {
|
||||||
Ok((
|
Ok((
|
||||||
row.get::<_, i64>(0)?,
|
row.get::<_, i64>(0)?,
|
||||||
DailyStats {
|
DailyStats {
|
||||||
@@ -293,24 +312,33 @@ impl Database {
|
|||||||
total_cache_read_tokens: row.get::<_, i64>(7)? as u64,
|
total_cache_read_tokens: row.get::<_, i64>(7)? as u64,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})?;
|
};
|
||||||
|
|
||||||
let mut map: HashMap<i64, DailyStats> = HashMap::new();
|
let mut map: HashMap<i64, DailyStats> = HashMap::new();
|
||||||
for row in rows {
|
|
||||||
let (mut bucket_idx, stat) = row?;
|
// Collect rows into map (need to handle both param variants)
|
||||||
if bucket_idx < 0 {
|
{
|
||||||
continue;
|
let rows = if let Some(at) = app_type {
|
||||||
|
stmt.query_map(params![start_ts, end_ts, bucket_seconds, at], row_mapper)?
|
||||||
|
} else {
|
||||||
|
stmt.query_map(params![start_ts, end_ts, bucket_seconds], row_mapper)?
|
||||||
|
};
|
||||||
|
for row in rows {
|
||||||
|
let (mut bucket_idx, stat) = row?;
|
||||||
|
if bucket_idx < 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if bucket_idx >= bucket_count {
|
||||||
|
bucket_idx = bucket_count - 1;
|
||||||
|
}
|
||||||
|
map.insert(bucket_idx, stat);
|
||||||
}
|
}
|
||||||
if bucket_idx >= bucket_count {
|
|
||||||
bucket_idx = bucket_count - 1;
|
|
||||||
}
|
|
||||||
map.insert(bucket_idx, stat);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also query rollup data (daily granularity, only useful for daily buckets)
|
// Also query rollup data (daily granularity, only useful for daily buckets)
|
||||||
if bucket_seconds >= 86400 {
|
if bucket_seconds >= 86400 {
|
||||||
let rollup_sql = "
|
let rollup_sql = format!(
|
||||||
SELECT
|
"SELECT
|
||||||
CAST((CAST(strftime('%s', date) AS INTEGER) - ?1) / ?3 AS INTEGER) as bucket_idx,
|
CAST((CAST(strftime('%s', date) AS INTEGER) - ?1) / ?3 AS INTEGER) as bucket_idx,
|
||||||
COALESCE(SUM(request_count), 0),
|
COALESCE(SUM(request_count), 0),
|
||||||
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0),
|
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0),
|
||||||
@@ -320,12 +348,12 @@ impl Database {
|
|||||||
COALESCE(SUM(cache_creation_tokens), 0),
|
COALESCE(SUM(cache_creation_tokens), 0),
|
||||||
COALESCE(SUM(cache_read_tokens), 0)
|
COALESCE(SUM(cache_read_tokens), 0)
|
||||||
FROM usage_daily_rollups
|
FROM usage_daily_rollups
|
||||||
WHERE date >= date(?1, 'unixepoch', 'localtime') AND date <= date(?2, 'unixepoch', 'localtime')
|
WHERE date >= date(?1, 'unixepoch', 'localtime') AND date <= date(?2, 'unixepoch', 'localtime') {app_type_filter}
|
||||||
GROUP BY bucket_idx
|
GROUP BY bucket_idx
|
||||||
ORDER BY bucket_idx ASC";
|
ORDER BY bucket_idx ASC"
|
||||||
|
);
|
||||||
|
|
||||||
let mut rstmt = conn.prepare(rollup_sql)?;
|
let rollup_row_mapper = |row: &rusqlite::Row| {
|
||||||
let rrows = rstmt.query_map(params![start_ts, end_ts, bucket_seconds], |row| {
|
|
||||||
Ok((
|
Ok((
|
||||||
row.get::<_, i64>(0)?,
|
row.get::<_, i64>(0)?,
|
||||||
(
|
(
|
||||||
@@ -338,7 +366,14 @@ impl Database {
|
|||||||
row.get::<_, i64>(7)? as u64,
|
row.get::<_, i64>(7)? as u64,
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
})?;
|
};
|
||||||
|
|
||||||
|
let mut rstmt = conn.prepare(&rollup_sql)?;
|
||||||
|
let rrows = if let Some(at) = app_type {
|
||||||
|
rstmt.query_map(params![start_ts, end_ts, bucket_seconds, at], rollup_row_mapper)?
|
||||||
|
} else {
|
||||||
|
rstmt.query_map(params![start_ts, end_ts, bucket_seconds], rollup_row_mapper)?
|
||||||
|
};
|
||||||
|
|
||||||
for row in rrows {
|
for row in rrows {
|
||||||
let (mut bucket_idx, (req, cost, tok, inp, out, cc, cr)) = row?;
|
let (mut bucket_idx, (req, cost, tok, inp, out, cc, cr)) = row?;
|
||||||
@@ -400,11 +435,21 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 Provider 统计
|
/// 获取 Provider 统计
|
||||||
pub fn get_provider_stats(&self) -> Result<Vec<ProviderStats>, AppError> {
|
pub fn get_provider_stats(
|
||||||
|
&self,
|
||||||
|
app_type: Option<&str>,
|
||||||
|
) -> Result<Vec<ProviderStats>, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
|
let (detail_where, rollup_where) = if app_type.is_some() {
|
||||||
|
("WHERE l.app_type = ?1", "WHERE r.app_type = ?2")
|
||||||
|
} else {
|
||||||
|
("", "")
|
||||||
|
};
|
||||||
|
|
||||||
// UNION detail logs + rollup data, then aggregate
|
// UNION detail logs + rollup data, then aggregate
|
||||||
let sql = "SELECT
|
let sql = format!(
|
||||||
|
"SELECT
|
||||||
provider_id, app_type, provider_name,
|
provider_id, app_type, provider_name,
|
||||||
SUM(request_count) as request_count,
|
SUM(request_count) as request_count,
|
||||||
SUM(total_tokens) as total_tokens,
|
SUM(total_tokens) as total_tokens,
|
||||||
@@ -423,6 +468,7 @@ impl Database {
|
|||||||
COALESCE(SUM(l.latency_ms), 0) as latency_sum
|
COALESCE(SUM(l.latency_ms), 0) as latency_sum
|
||||||
FROM proxy_request_logs l
|
FROM proxy_request_logs l
|
||||||
LEFT JOIN providers p ON l.provider_id = p.id AND l.app_type = p.app_type
|
LEFT JOIN providers p ON l.provider_id = p.id AND l.app_type = p.app_type
|
||||||
|
{detail_where}
|
||||||
GROUP BY l.provider_id, l.app_type
|
GROUP BY l.provider_id, l.app_type
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT r.provider_id, r.app_type,
|
SELECT r.provider_id, r.app_type,
|
||||||
@@ -434,13 +480,15 @@ impl Database {
|
|||||||
COALESCE(SUM(r.avg_latency_ms * r.request_count), 0)
|
COALESCE(SUM(r.avg_latency_ms * r.request_count), 0)
|
||||||
FROM usage_daily_rollups r
|
FROM usage_daily_rollups r
|
||||||
LEFT JOIN providers p2 ON r.provider_id = p2.id AND r.app_type = p2.app_type
|
LEFT JOIN providers p2 ON r.provider_id = p2.id AND r.app_type = p2.app_type
|
||||||
|
{rollup_where}
|
||||||
GROUP BY r.provider_id, r.app_type
|
GROUP BY r.provider_id, r.app_type
|
||||||
)
|
)
|
||||||
GROUP BY provider_id, app_type
|
GROUP BY provider_id, app_type
|
||||||
ORDER BY total_cost DESC";
|
ORDER BY total_cost DESC"
|
||||||
|
);
|
||||||
|
|
||||||
let mut stmt = conn.prepare(sql)?;
|
let mut stmt = conn.prepare(&sql)?;
|
||||||
let rows = stmt.query_map([], |row| {
|
let row_mapper = |row: &rusqlite::Row| {
|
||||||
let request_count: i64 = row.get(3)?;
|
let request_count: i64 = row.get(3)?;
|
||||||
let success_count: i64 = row.get(6)?;
|
let success_count: i64 = row.get(6)?;
|
||||||
let success_rate = if request_count > 0 {
|
let success_rate = if request_count > 0 {
|
||||||
@@ -460,7 +508,13 @@ impl Database {
|
|||||||
success_rate,
|
success_rate,
|
||||||
avg_latency_ms: row.get::<_, f64>(7)? as u64,
|
avg_latency_ms: row.get::<_, f64>(7)? as u64,
|
||||||
})
|
})
|
||||||
})?;
|
};
|
||||||
|
|
||||||
|
let rows = if let Some(at) = app_type {
|
||||||
|
stmt.query_map(params![at, at], row_mapper)?
|
||||||
|
} else {
|
||||||
|
stmt.query_map([], row_mapper)?
|
||||||
|
};
|
||||||
|
|
||||||
let mut stats = Vec::new();
|
let mut stats = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
@@ -471,11 +525,18 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 获取模型统计
|
/// 获取模型统计
|
||||||
pub fn get_model_stats(&self) -> Result<Vec<ModelStats>, AppError> {
|
pub fn get_model_stats(&self, app_type: Option<&str>) -> Result<Vec<ModelStats>, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
|
let (detail_where, rollup_where) = if app_type.is_some() {
|
||||||
|
("WHERE app_type = ?1", "WHERE app_type = ?2")
|
||||||
|
} else {
|
||||||
|
("", "")
|
||||||
|
};
|
||||||
|
|
||||||
// UNION detail logs + rollup data
|
// UNION detail logs + rollup data
|
||||||
let sql = "SELECT
|
let sql = format!(
|
||||||
|
"SELECT
|
||||||
model,
|
model,
|
||||||
SUM(request_count) as request_count,
|
SUM(request_count) as request_count,
|
||||||
SUM(total_tokens) as total_tokens,
|
SUM(total_tokens) as total_tokens,
|
||||||
@@ -486,6 +547,7 @@ impl Database {
|
|||||||
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
||||||
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost
|
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost
|
||||||
FROM proxy_request_logs
|
FROM proxy_request_logs
|
||||||
|
{detail_where}
|
||||||
GROUP BY model
|
GROUP BY model
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT model,
|
SELECT model,
|
||||||
@@ -493,13 +555,15 @@ impl Database {
|
|||||||
COALESCE(SUM(input_tokens + output_tokens), 0),
|
COALESCE(SUM(input_tokens + output_tokens), 0),
|
||||||
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0)
|
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0)
|
||||||
FROM usage_daily_rollups
|
FROM usage_daily_rollups
|
||||||
|
{rollup_where}
|
||||||
GROUP BY model
|
GROUP BY model
|
||||||
)
|
)
|
||||||
GROUP BY model
|
GROUP BY model
|
||||||
ORDER BY total_cost DESC";
|
ORDER BY total_cost DESC"
|
||||||
|
);
|
||||||
|
|
||||||
let mut stmt = conn.prepare(sql)?;
|
let mut stmt = conn.prepare(&sql)?;
|
||||||
let rows = stmt.query_map([], |row| {
|
let row_mapper = |row: &rusqlite::Row| {
|
||||||
let request_count: i64 = row.get(1)?;
|
let request_count: i64 = row.get(1)?;
|
||||||
let total_cost: f64 = row.get(3)?;
|
let total_cost: f64 = row.get(3)?;
|
||||||
let avg_cost = if request_count > 0 {
|
let avg_cost = if request_count > 0 {
|
||||||
@@ -515,7 +579,13 @@ impl Database {
|
|||||||
total_cost: format!("{total_cost:.6}"),
|
total_cost: format!("{total_cost:.6}"),
|
||||||
avg_cost_per_request: format!("{avg_cost:.6}"),
|
avg_cost_per_request: format!("{avg_cost:.6}"),
|
||||||
})
|
})
|
||||||
})?;
|
};
|
||||||
|
|
||||||
|
let rows = if let Some(at) = app_type {
|
||||||
|
stmt.query_map(params![at, at], row_mapper)?
|
||||||
|
} else {
|
||||||
|
stmt.query_map([], row_mapper)?
|
||||||
|
};
|
||||||
|
|
||||||
let mut stats = Vec::new();
|
let mut stats = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
@@ -1044,7 +1114,7 @@ mod tests {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let summary = db.get_usage_summary(None, None)?;
|
let summary = db.get_usage_summary(None, None, None)?;
|
||||||
assert_eq!(summary.total_requests, 2);
|
assert_eq!(summary.total_requests, 2);
|
||||||
assert_eq!(summary.success_rate, 100.0);
|
assert_eq!(summary.success_rate, 100.0);
|
||||||
|
|
||||||
@@ -1079,7 +1149,7 @@ mod tests {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stats = db.get_model_stats()?;
|
let stats = db.get_model_stats(None)?;
|
||||||
assert_eq!(stats.len(), 1);
|
assert_eq!(stats.len(), 1);
|
||||||
assert_eq!(stats[0].model, "claude-3-sonnet");
|
assert_eq!(stats[0].model, "claude-3-sonnet");
|
||||||
assert_eq!(stats[0].request_count, 1);
|
assert_eq!(stats[0].request_count, 1);
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ import { useModelStats } from "@/lib/query/usage";
|
|||||||
import { fmtUsd } from "./format";
|
import { fmtUsd } from "./format";
|
||||||
|
|
||||||
interface ModelStatsTableProps {
|
interface ModelStatsTableProps {
|
||||||
|
appType?: string;
|
||||||
refreshIntervalMs: number;
|
refreshIntervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModelStatsTable({ refreshIntervalMs }: ModelStatsTableProps) {
|
export function ModelStatsTable({
|
||||||
|
appType,
|
||||||
|
refreshIntervalMs,
|
||||||
|
}: ModelStatsTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: stats, isLoading } = useModelStats({
|
const { data: stats, isLoading } = useModelStats(appType, {
|
||||||
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ import { useProviderStats } from "@/lib/query/usage";
|
|||||||
import { fmtUsd } from "./format";
|
import { fmtUsd } from "./format";
|
||||||
|
|
||||||
interface ProviderStatsTableProps {
|
interface ProviderStatsTableProps {
|
||||||
|
appType?: string;
|
||||||
refreshIntervalMs: number;
|
refreshIntervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProviderStatsTable({
|
export function ProviderStatsTable({
|
||||||
|
appType,
|
||||||
refreshIntervalMs,
|
refreshIntervalMs,
|
||||||
}: ProviderStatsTableProps) {
|
}: ProviderStatsTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: stats, isLoading } = useProviderStats({
|
const { data: stats, isLoading } = useProviderStats(appType, {
|
||||||
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from "./format";
|
} from "./format";
|
||||||
|
|
||||||
interface RequestLogTableProps {
|
interface RequestLogTableProps {
|
||||||
|
appType?: string;
|
||||||
refreshIntervalMs: number;
|
refreshIntervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +38,10 @@ const MAX_FIXED_RANGE_SECONDS = 30 * ONE_DAY_SECONDS;
|
|||||||
|
|
||||||
type TimeMode = "rolling" | "fixed";
|
type TimeMode = "rolling" | "fixed";
|
||||||
|
|
||||||
export function RequestLogTable({ refreshIntervalMs }: RequestLogTableProps) {
|
export function RequestLogTable({
|
||||||
|
appType: dashboardAppType,
|
||||||
|
refreshIntervalMs,
|
||||||
|
}: RequestLogTableProps) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -56,8 +60,14 @@ export function RequestLogTable({ refreshIntervalMs }: RequestLogTableProps) {
|
|||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
const [validationError, setValidationError] = useState<string | null>(null);
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// When dashboard-level app filter is active (not "all"), override the local appType filter
|
||||||
|
const dashboardAppTypeActive = dashboardAppType && dashboardAppType !== "all";
|
||||||
|
const effectiveFilters: LogFilters = dashboardAppTypeActive
|
||||||
|
? { ...appliedFilters, appType: dashboardAppType }
|
||||||
|
: appliedFilters;
|
||||||
|
|
||||||
const { data: result, isLoading } = useRequestLogs({
|
const { data: result, isLoading } = useRequestLogs({
|
||||||
filters: appliedFilters,
|
filters: effectiveFilters,
|
||||||
timeMode: appliedTimeMode,
|
timeMode: appliedTimeMode,
|
||||||
rollingWindowSeconds: ONE_DAY_SECONDS,
|
rollingWindowSeconds: ONE_DAY_SECONDS,
|
||||||
page,
|
page,
|
||||||
@@ -174,13 +184,18 @@ export function RequestLogTable({ refreshIntervalMs }: RequestLogTableProps) {
|
|||||||
<div className="flex flex-col gap-4 rounded-lg border bg-card/50 p-4 backdrop-blur-sm">
|
<div className="flex flex-col gap-4 rounded-lg border bg-card/50 p-4 backdrop-blur-sm">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<Select
|
<Select
|
||||||
value={draftFilters.appType || "all"}
|
value={
|
||||||
|
dashboardAppTypeActive
|
||||||
|
? dashboardAppType
|
||||||
|
: draftFilters.appType || "all"
|
||||||
|
}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
setDraftFilters({
|
setDraftFilters({
|
||||||
...draftFilters,
|
...draftFilters,
|
||||||
appType: v === "all" ? undefined : v,
|
appType: v === "all" ? undefined : v,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
disabled={!!dashboardAppTypeActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[130px] bg-background">
|
<SelectTrigger className="w-[130px] bg-background">
|
||||||
<SelectValue placeholder={t("usage.appType")} />
|
<SelectValue placeholder={t("usage.appType")} />
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { UsageTrendChart } from "./UsageTrendChart";
|
|||||||
import { RequestLogTable } from "./RequestLogTable";
|
import { RequestLogTable } from "./RequestLogTable";
|
||||||
import { ProviderStatsTable } from "./ProviderStatsTable";
|
import { ProviderStatsTable } from "./ProviderStatsTable";
|
||||||
import { ModelStatsTable } from "./ModelStatsTable";
|
import { ModelStatsTable } from "./ModelStatsTable";
|
||||||
import { DataSourceBar } from "./DataSourceBar";
|
import type { AppTypeFilter, TimeRange } from "@/types/usage";
|
||||||
import type { TimeRange } from "@/types/usage";
|
import { useUsageSummary } from "@/lib/query/usage";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
@@ -26,11 +26,21 @@ import {
|
|||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { PricingConfigPanel } from "@/components/usage/PricingConfigPanel";
|
import { PricingConfigPanel } from "@/components/usage/PricingConfigPanel";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { fmtUsd, parseFiniteNumber } from "./format";
|
||||||
|
|
||||||
|
const APP_FILTER_OPTIONS: AppTypeFilter[] = [
|
||||||
|
"all",
|
||||||
|
"claude",
|
||||||
|
"codex",
|
||||||
|
"gemini",
|
||||||
|
];
|
||||||
|
|
||||||
export function UsageDashboard() {
|
export function UsageDashboard() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
|
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
|
||||||
|
const [appType, setAppType] = useState<AppTypeFilter>("all");
|
||||||
const [refreshIntervalMs, setRefreshIntervalMs] = useState(30000);
|
const [refreshIntervalMs, setRefreshIntervalMs] = useState(30000);
|
||||||
|
|
||||||
const refreshIntervalOptionsMs = [0, 5000, 10000, 30000, 60000] as const;
|
const refreshIntervalOptionsMs = [0, 5000, 10000, 30000, 60000] as const;
|
||||||
@@ -47,6 +57,11 @@ export function UsageDashboard() {
|
|||||||
|
|
||||||
const days = timeRange === "1d" ? 1 : timeRange === "7d" ? 7 : 30;
|
const days = timeRange === "1d" ? 1 : timeRange === "7d" ? 7 : 30;
|
||||||
|
|
||||||
|
// Summary data for the app filter bar
|
||||||
|
const { data: summaryData } = useUsageSummary(days, appType, {
|
||||||
|
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
@@ -101,11 +116,49 @@ export function UsageDashboard() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataSourceBar refreshIntervalMs={refreshIntervalMs} />
|
{/* App type filter bar (replaces DataSourceBar) */}
|
||||||
|
<div className="rounded-xl border border-border/50 bg-card/40 backdrop-blur-sm p-4 space-y-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
{APP_FILTER_OPTIONS.map((type) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAppType(type)}
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-1.5 rounded-lg text-sm font-medium transition-all",
|
||||||
|
appType === type
|
||||||
|
? "bg-primary/10 text-primary shadow-sm border border-primary/20"
|
||||||
|
: "text-muted-foreground hover:text-primary hover:bg-muted/50 border border-transparent",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t(`usage.appFilter.${type}`)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
{(summaryData?.totalRequests ?? 0).toLocaleString()}{" "}
|
||||||
|
{t("usage.requestsLabel")}
|
||||||
|
</span>
|
||||||
|
<span className="text-border">|</span>
|
||||||
|
<span>
|
||||||
|
{fmtUsd(parseFiniteNumber(summaryData?.totalCost) ?? 0, 4)}{" "}
|
||||||
|
{t("usage.costLabel")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UsageSummaryCards days={days} refreshIntervalMs={refreshIntervalMs} />
|
<UsageSummaryCards
|
||||||
|
days={days}
|
||||||
|
appType={appType}
|
||||||
|
refreshIntervalMs={refreshIntervalMs}
|
||||||
|
/>
|
||||||
|
|
||||||
<UsageTrendChart days={days} refreshIntervalMs={refreshIntervalMs} />
|
<UsageTrendChart
|
||||||
|
days={days}
|
||||||
|
appType={appType}
|
||||||
|
refreshIntervalMs={refreshIntervalMs}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Tabs defaultValue="logs" className="w-full">
|
<Tabs defaultValue="logs" className="w-full">
|
||||||
@@ -132,15 +185,24 @@ export function UsageDashboard() {
|
|||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
>
|
>
|
||||||
<TabsContent value="logs" className="mt-0">
|
<TabsContent value="logs" className="mt-0">
|
||||||
<RequestLogTable refreshIntervalMs={refreshIntervalMs} />
|
<RequestLogTable
|
||||||
|
appType={appType}
|
||||||
|
refreshIntervalMs={refreshIntervalMs}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="providers" className="mt-0">
|
<TabsContent value="providers" className="mt-0">
|
||||||
<ProviderStatsTable refreshIntervalMs={refreshIntervalMs} />
|
<ProviderStatsTable
|
||||||
|
appType={appType}
|
||||||
|
refreshIntervalMs={refreshIntervalMs}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="models" className="mt-0">
|
<TabsContent value="models" className="mt-0">
|
||||||
<ModelStatsTable refreshIntervalMs={refreshIntervalMs} />
|
<ModelStatsTable
|
||||||
|
appType={appType}
|
||||||
|
refreshIntervalMs={refreshIntervalMs}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ import { fmtUsd, parseFiniteNumber } from "./format";
|
|||||||
|
|
||||||
interface UsageSummaryCardsProps {
|
interface UsageSummaryCardsProps {
|
||||||
days: number;
|
days: number;
|
||||||
|
appType?: string;
|
||||||
refreshIntervalMs: number;
|
refreshIntervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsageSummaryCards({
|
export function UsageSummaryCards({
|
||||||
days,
|
days,
|
||||||
|
appType,
|
||||||
refreshIntervalMs,
|
refreshIntervalMs,
|
||||||
}: UsageSummaryCardsProps) {
|
}: UsageSummaryCardsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data: summary, isLoading } = useUsageSummary(days, {
|
const { data: summary, isLoading } = useUsageSummary(days, appType, {
|
||||||
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,15 +20,17 @@ import {
|
|||||||
|
|
||||||
interface UsageTrendChartProps {
|
interface UsageTrendChartProps {
|
||||||
days: number;
|
days: number;
|
||||||
|
appType?: string;
|
||||||
refreshIntervalMs: number;
|
refreshIntervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsageTrendChart({
|
export function UsageTrendChart({
|
||||||
days,
|
days,
|
||||||
|
appType,
|
||||||
refreshIntervalMs,
|
refreshIntervalMs,
|
||||||
}: UsageTrendChartProps) {
|
}: UsageTrendChartProps) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { data: trends, isLoading } = useUsageTrends(days, {
|
const { data: trends, isLoading } = useUsageTrends(days, appType, {
|
||||||
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1019,6 +1019,14 @@
|
|||||||
"stream": "Stream",
|
"stream": "Stream",
|
||||||
"nonStream": "Non-stream",
|
"nonStream": "Non-stream",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
|
"requestsLabel": "requests",
|
||||||
|
"costLabel": "total cost",
|
||||||
|
"appFilter": {
|
||||||
|
"all": "All",
|
||||||
|
"claude": "Claude Code",
|
||||||
|
"codex": "Codex",
|
||||||
|
"gemini": "Gemini"
|
||||||
|
},
|
||||||
"dataSources": "Data Sources",
|
"dataSources": "Data Sources",
|
||||||
"dataSource": {
|
"dataSource": {
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
|||||||
@@ -1019,6 +1019,14 @@
|
|||||||
"stream": "ストリーム",
|
"stream": "ストリーム",
|
||||||
"nonStream": "非ストリーム",
|
"nonStream": "非ストリーム",
|
||||||
"source": "ソース",
|
"source": "ソース",
|
||||||
|
"requestsLabel": "リクエスト",
|
||||||
|
"costLabel": "合計コスト",
|
||||||
|
"appFilter": {
|
||||||
|
"all": "すべて",
|
||||||
|
"claude": "Claude Code",
|
||||||
|
"codex": "Codex",
|
||||||
|
"gemini": "Gemini"
|
||||||
|
},
|
||||||
"dataSources": "データソース",
|
"dataSources": "データソース",
|
||||||
"dataSource": {
|
"dataSource": {
|
||||||
"proxy": "プロキシ",
|
"proxy": "プロキシ",
|
||||||
|
|||||||
@@ -1019,6 +1019,14 @@
|
|||||||
"stream": "流",
|
"stream": "流",
|
||||||
"nonStream": "非流",
|
"nonStream": "非流",
|
||||||
"source": "来源",
|
"source": "来源",
|
||||||
|
"requestsLabel": "次请求",
|
||||||
|
"costLabel": "总成本",
|
||||||
|
"appFilter": {
|
||||||
|
"all": "全部",
|
||||||
|
"claude": "Claude Code",
|
||||||
|
"codex": "Codex",
|
||||||
|
"gemini": "Gemini"
|
||||||
|
},
|
||||||
"dataSources": "数据来源",
|
"dataSources": "数据来源",
|
||||||
"dataSource": {
|
"dataSource": {
|
||||||
"proxy": "代理",
|
"proxy": "代理",
|
||||||
|
|||||||
@@ -50,23 +50,25 @@ export const usageApi = {
|
|||||||
getUsageSummary: async (
|
getUsageSummary: async (
|
||||||
startDate?: number,
|
startDate?: number,
|
||||||
endDate?: number,
|
endDate?: number,
|
||||||
|
appType?: string,
|
||||||
): Promise<UsageSummary> => {
|
): Promise<UsageSummary> => {
|
||||||
return invoke("get_usage_summary", { startDate, endDate });
|
return invoke("get_usage_summary", { startDate, endDate, appType });
|
||||||
},
|
},
|
||||||
|
|
||||||
getUsageTrends: async (
|
getUsageTrends: async (
|
||||||
startDate?: number,
|
startDate?: number,
|
||||||
endDate?: number,
|
endDate?: number,
|
||||||
|
appType?: string,
|
||||||
): Promise<DailyStats[]> => {
|
): Promise<DailyStats[]> => {
|
||||||
return invoke("get_usage_trends", { startDate, endDate });
|
return invoke("get_usage_trends", { startDate, endDate, appType });
|
||||||
},
|
},
|
||||||
|
|
||||||
getProviderStats: async (): Promise<ProviderStats[]> => {
|
getProviderStats: async (appType?: string): Promise<ProviderStats[]> => {
|
||||||
return invoke("get_provider_stats");
|
return invoke("get_provider_stats", { appType });
|
||||||
},
|
},
|
||||||
|
|
||||||
getModelStats: async (): Promise<ModelStats[]> => {
|
getModelStats: async (appType?: string): Promise<ModelStats[]> => {
|
||||||
return invoke("get_model_stats");
|
return invoke("get_model_stats", { appType });
|
||||||
},
|
},
|
||||||
|
|
||||||
getRequestLogs: async (
|
getRequestLogs: async (
|
||||||
|
|||||||
+47
-28
@@ -34,10 +34,14 @@ type RequestLogsKey = {
|
|||||||
// Query keys
|
// Query keys
|
||||||
export const usageKeys = {
|
export const usageKeys = {
|
||||||
all: ["usage"] as const,
|
all: ["usage"] as const,
|
||||||
summary: (days: number) => [...usageKeys.all, "summary", days] as const,
|
summary: (days: number, appType?: string) =>
|
||||||
trends: (days: number) => [...usageKeys.all, "trends", days] as const,
|
[...usageKeys.all, "summary", days, appType ?? "all"] as const,
|
||||||
providerStats: () => [...usageKeys.all, "provider-stats"] as const,
|
trends: (days: number, appType?: string) =>
|
||||||
modelStats: () => [...usageKeys.all, "model-stats"] as const,
|
[...usageKeys.all, "trends", days, appType ?? "all"] as const,
|
||||||
|
providerStats: (appType?: string) =>
|
||||||
|
[...usageKeys.all, "provider-stats", appType ?? "all"] as const,
|
||||||
|
modelStats: (appType?: string) =>
|
||||||
|
[...usageKeys.all, "model-stats", appType ?? "all"] as const,
|
||||||
logs: (key: RequestLogsKey, page: number, pageSize: number) =>
|
logs: (key: RequestLogsKey, page: number, pageSize: number) =>
|
||||||
[
|
[
|
||||||
...usageKeys.all,
|
...usageKeys.all,
|
||||||
@@ -67,44 +71,59 @@ const getWindow = (days: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
export function useUsageSummary(days: number, options?: UsageQueryOptions) {
|
export function useUsageSummary(
|
||||||
|
days: number,
|
||||||
|
appType?: string,
|
||||||
|
options?: UsageQueryOptions,
|
||||||
|
) {
|
||||||
|
const effectiveAppType = appType === "all" ? undefined : appType;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: usageKeys.summary(days),
|
queryKey: usageKeys.summary(days, appType),
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const { startDate, endDate } = getWindow(days);
|
const { startDate, endDate } = getWindow(days);
|
||||||
return usageApi.getUsageSummary(startDate, endDate);
|
return usageApi.getUsageSummary(startDate, endDate, effectiveAppType);
|
||||||
},
|
},
|
||||||
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
|
||||||
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false, // 后台不刷新
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUsageTrends(days: number, options?: UsageQueryOptions) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: usageKeys.trends(days),
|
|
||||||
queryFn: () => {
|
|
||||||
const { startDate, endDate } = getWindow(days);
|
|
||||||
return usageApi.getUsageTrends(startDate, endDate);
|
|
||||||
},
|
|
||||||
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
|
||||||
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProviderStats(options?: UsageQueryOptions) {
|
export function useUsageTrends(
|
||||||
|
days: number,
|
||||||
|
appType?: string,
|
||||||
|
options?: UsageQueryOptions,
|
||||||
|
) {
|
||||||
|
const effectiveAppType = appType === "all" ? undefined : appType;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: usageKeys.providerStats(),
|
queryKey: usageKeys.trends(days, appType),
|
||||||
queryFn: usageApi.getProviderStats,
|
queryFn: () => {
|
||||||
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
const { startDate, endDate } = getWindow(days);
|
||||||
|
return usageApi.getUsageTrends(startDate, endDate, effectiveAppType);
|
||||||
|
},
|
||||||
|
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
|
||||||
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModelStats(options?: UsageQueryOptions) {
|
export function useProviderStats(
|
||||||
|
appType?: string,
|
||||||
|
options?: UsageQueryOptions,
|
||||||
|
) {
|
||||||
|
const effectiveAppType = appType === "all" ? undefined : appType;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: usageKeys.modelStats(),
|
queryKey: usageKeys.providerStats(appType),
|
||||||
queryFn: usageApi.getModelStats,
|
queryFn: () => usageApi.getProviderStats(effectiveAppType),
|
||||||
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
|
||||||
|
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useModelStats(appType?: string, options?: UsageQueryOptions) {
|
||||||
|
const effectiveAppType = appType === "all" ? undefined : appType;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: usageKeys.modelStats(appType),
|
||||||
|
queryFn: () => usageApi.getModelStats(effectiveAppType),
|
||||||
|
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
|
||||||
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ export interface ProviderLimitStatus {
|
|||||||
|
|
||||||
export type TimeRange = "1d" | "7d" | "30d";
|
export type TimeRange = "1d" | "7d" | "30d";
|
||||||
|
|
||||||
|
export type AppTypeFilter = "all" | "claude" | "codex" | "gemini";
|
||||||
|
|
||||||
export interface StatsFilters {
|
export interface StatsFilters {
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
providerId?: string;
|
providerId?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user