feat(account): display OpenRouter usage on account page

Show cumulative cost, API call count, and total tokens in card
layout when the user has AI usage data.
This commit is contained in:
Jon Seager 2026-02-05 12:09:43 +00:00
parent 0a93df845f
commit 9eb9a9a56c
No known key found for this signature in database
2 changed files with 65 additions and 0 deletions

View file

@ -16,6 +16,36 @@ use super::auth::is_authenticated;
// --- View types --- // --- View types ---
#[derive(Serialize)]
pub struct AiUsageView {
pub total_calls: i64,
pub total_tokens: String,
pub total_cost: String,
}
fn format_cost(cost: f64) -> String {
if cost < 0.01 {
format!("${cost:.4}")
} else {
format!("${cost:.2}")
}
}
fn format_number(n: i64) -> String {
if n < 1_000 {
return n.to_string();
}
let s = n.to_string();
let mut result = String::with_capacity(s.len() + s.len() / 3);
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(c);
}
result.chars().rev().collect()
}
#[derive(Serialize)] #[derive(Serialize)]
pub struct PasskeyView { pub struct PasskeyView {
pub id: i64, pub id: i64,
@ -43,6 +73,7 @@ fn format_date(dt: DateTime<Utc>) -> String {
struct AccountTemplate { struct AccountTemplate {
nav_active: &'static str, nav_active: &'static str,
is_authenticated: bool, is_authenticated: bool,
ai_usage: Option<AiUsageView>,
passkeys: Vec<PasskeyView>, passkeys: Vec<PasskeyView>,
tokens: Vec<TokenView>, tokens: Vec<TokenView>,
} }
@ -91,9 +122,22 @@ pub(crate) async fn account_page(
}) })
.collect(); .collect();
let ai_usage = state
.ai_usage_repo
.summary_for_user(auth_user.id)
.await
.ok()
.filter(|s| s.total_calls > 0)
.map(|s| AiUsageView {
total_calls: s.total_calls,
total_tokens: format_number(s.total_tokens),
total_cost: format_cost(s.total_cost),
});
let template = AccountTemplate { let template = AccountTemplate {
nav_active: "account", nav_active: "account",
is_authenticated: true, is_authenticated: true,
ai_usage,
passkeys, passkeys,
tokens, tokens,
}; };

View file

@ -212,6 +212,27 @@
<div id="backup-error" class="mt-3 hidden rounded-md bg-red-100 border border-red-300 p-3 text-sm text-red-800"></div> <div id="backup-error" class="mt-3 hidden rounded-md bg-red-100 border border-red-300 p-3 text-sm text-red-800"></div>
</section> </section>
<!-- OpenRouter Usage -->
{% if let Some(usage) = ai_usage %}
<section>
<h2 class="text-lg font-semibold text-amber-700 mb-3">OpenRouter Usage</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 pt-4 pb-3 shadow-sm">
<span class="block text-sm text-stone-500">Total Cost</span>
<span class="mt-1 block text-lg font-semibold text-amber-800">{{ usage.total_cost }}</span>
</div>
<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 pt-4 pb-3 shadow-sm">
<span class="block text-sm text-stone-500">API Calls</span>
<span class="mt-1 block text-lg font-semibold text-amber-800">{{ usage.total_calls }}</span>
</div>
<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 pt-4 pb-3 shadow-sm">
<span class="block text-sm text-stone-500">Total Tokens</span>
<span class="mt-1 block text-lg font-semibold text-amber-800">{{ usage.total_tokens }}</span>
</div>
</div>
</section>
{% endif %}
<!-- Sign Out --> <!-- Sign Out -->
<section> <section>
<form method="post" action="/logout"> <form method="post" action="/logout">