-
{event.model}
-
{event.status}
+
+
+ {events.map((event) => (
+
+
+
+ {event.model}
+ {event.status}
+
+
+ {`${accountNameByID.get(event.account_id) ?? t("账户")} · #${event.account_id}`}
+ {new Date(event.created_at).toLocaleString(language, { hour12: false })}
+ {Math.round(event.latency_ms)} ms
+
-
-
{`${accountNameByID.get(event.account_id) ?? t("账户")} · #${event.account_id}`}
-
{new Date(event.created_at).toLocaleString(language, { hour12: false })}
-
{Math.round(event.latency_ms)} ms
+
+ {formatCompactNumber(language, event.total_tokens)} tok
+ {formatCurrency(language, event.estimated_cost)}
-
- {formatCompactNumber(language, event.total_tokens)} tok
- {formatCurrency(language, event.estimated_cost)}
-
-
- ))}
+ ))}
+
+ {eventsPage.total > eventsPage.page_size ? (
+
setEventsPage((current) => ({ ...current, page }))}
+ />
+ ) : null}
)}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 1cf6fc1..f03e7f5 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -111,6 +111,18 @@ export type UsageDashboardSummary = {
quota_delta: number;
};
+export type UsageRequestQuality = {
+ request_count: number;
+ success_count: number;
+ failure_count: number;
+ success_rate: number;
+ avg_latency_ms: number;
+ p95_latency_ms: number;
+ p99_latency_ms: number;
+ min_latency_ms: number;
+ max_latency_ms: number;
+};
+
export type ServerSession = {
authenticated: boolean;
role?: "admin" | "user";
@@ -242,6 +254,13 @@ export type UsageEventRecord = {
created_at: string;
};
+export type UsageEventPage = {
+ items: UsageEventRecord[];
+ total: number;
+ page: number;
+ page_size: number;
+};
+
export type PricingRule = {
input_per_million: number;
output_per_million: number;
@@ -706,6 +725,8 @@ function dashboardQuery(
model?: string,
limit?: number,
serverUserID?: number,
+ page?: number,
+ pageSize?: number,
): string {
const params = new URLSearchParams();
params.set("range", range);
@@ -721,6 +742,12 @@ function dashboardQuery(
if (limit && limit > 0) {
params.set("limit", String(limit));
}
+ if (page && page > 0) {
+ params.set("page", String(page));
+ }
+ if (pageSize && pageSize > 0) {
+ params.set("page_size", String(pageSize));
+ }
return params.toString();
}
@@ -739,6 +766,21 @@ export async function getDashboardSummary(
return response.json();
}
+export async function getDashboardRequestQuality(
+ range: DashboardRangeKey = "24h",
+ accountID?: number,
+ model?: string,
+ serverUserID?: number,
+): Promise
{
+ const response = await fetch(
+ apiPath(`/dashboard/request-quality?${dashboardQuery(range, accountID, model, undefined, serverUserID)}`),
+ );
+ if (!response.ok) {
+ throw new Error("failed to load dashboard request quality");
+ }
+ return response.json();
+}
+
export async function getDashboardTrends(
range: DashboardRangeKey = "24h",
accountID?: number,
@@ -758,18 +800,28 @@ export async function getDashboardRecentEvents(
range: DashboardRangeKey = "24h",
accountID?: number,
model?: string,
- limit = 20,
+ page = 1,
+ pageSize = 20,
serverUserID?: number,
-): Promise {
+): Promise {
const response = await fetch(
apiPath(
- `/dashboard/recent-events?${dashboardQuery(range, accountID, model, limit, serverUserID)}`,
+ `/dashboard/recent-events?${dashboardQuery(range, accountID, model, undefined, serverUserID, page, pageSize)}`,
),
);
if (!response.ok) {
throw new Error("failed to load recent usage events");
}
- return response.json();
+ const payload = await response.json();
+ if (Array.isArray(payload)) {
+ return {
+ items: payload,
+ total: payload.length,
+ page,
+ page_size: pageSize,
+ };
+ }
+ return payload;
}
export async function getDashboardModelDistribution(
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index acdeec1..0514924 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -1650,6 +1650,10 @@ html[data-theme-mode="dark"] .top-home-update-dot {
gap: 12px;
}
+.stats-quality-grid {
+ grid-template-columns: repeat(auto-fit, minmax(188px, 1fr));
+}
+
.stats-summary-card,
.stats-panel {
border-radius: 16px;
@@ -1706,6 +1710,10 @@ html[data-theme-mode="dark"] .top-home-update-dot {
color: var(--text-secondary);
}
+.stats-quality-card .stats-card-value {
+ font-size: 20px;
+}
+
.stats-content-grid {
display: grid;
grid-template-columns: minmax(0, 1.8fr) minmax(280px, 0.9fr);
@@ -1718,6 +1726,16 @@ html[data-theme-mode="dark"] .top-home-update-dot {
gap: 12px;
}
+.stats-events-block {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.stats-events-pagination {
+ align-self: flex-end;
+}
+
.stats-event-meta,
.stats-event-metrics {
display: flex;