Budget setting and all dollar to chf
All checks were successful
Build and Push / build (push) Successful in 1m28s

This commit is contained in:
2026-05-02 22:33:35 +02:00
parent b77dd04b15
commit c7ab4c6b4e
2 changed files with 327 additions and 9 deletions

View File

@@ -2,6 +2,7 @@
import { useTranslations } from "next-intl";
import { useEffect, useState, useCallback } from "react";
import { BudgetEditableCard } from "@/components/dashboard/budget-editable-card";
interface DailyUsage {
date: string;
@@ -18,7 +19,17 @@ interface UsageData {
totalSpend: number;
requestCount: number;
};
budget: { maxBudget: number | null; spend: number; remaining: number | null };
budget: {
maxBudget: number | null;
spend: number;
remaining: number | null;
/**
* Feature 7: budget reset cadence as stored on LiteLLM.
* Strings: "30d" / "1mo" / "1y" / null (no reset). UI maps these
* to user-friendly labels.
*/
budgetDuration: string | null;
};
rateLimits: { rpm: number | null; tpm: number | null };
dailyUsage: DailyUsage[];
}
@@ -29,8 +40,31 @@ function fmt(n: number): string {
return n.toString();
}
function usd(n: number): string {
return `$${n.toFixed(4)}`;
/**
* Format a numeric amount as CHF.
*
* Note on currency labelling: LiteLLM stores raw cost numbers it
* receives from upstream (OpenAI/Anthropic), which originate as USD.
* The PieCed pricing config (Slice 5) converts those numbers to
* CHF before LiteLLM persists them, so the values flowing through
* here are already CHF amounts. We label them as such in the UI;
* "USD" or "$" anywhere in the customer-facing experience would
* be misleading.
*
* Precision is adaptive:
* - Amounts ≥ 1 CHF: 2 decimals (typical money formatting).
* - Smaller amounts: 4 decimals — per-request inference costs are
* routinely sub-rappen, and rounding to 2dp
* would render CHF 0.0042 as "CHF 0.00",
* which obscures real costs from customers
* looking at the daily breakdown.
*
* This is a customer-facing display helper; for storage and
* comparisons keep using the raw number.
*/
function chf(n: number): string {
const decimals = Math.abs(n) >= 1 ? 2 : 4;
return `CHF ${n.toFixed(decimals)}`;
}
function getCurrentMonth(): string {
@@ -69,7 +103,7 @@ function UsageChart({ data }: { data: DailyUsage[] }) {
const x = i * (barW + 2);
return (
<g key={d.date}>
<title>{d.date}: {fmt(d.inputTokens)} in / {fmt(d.outputTokens)} out {usd(d.spend)}</title>
<title>{d.date}: {fmt(d.inputTokens)} in / {fmt(d.outputTokens)} out {chf(d.spend)}</title>
<rect x={x} y={h - totalH} width={barW} height={totalH - inputH} rx={1} fill="var(--color-accent)" opacity={0.3} />
<rect x={x} y={h - inputH} width={barW} height={inputH} rx={1} fill="var(--color-accent)" opacity={0.7} />
{i % 7 === 0 && (
@@ -113,10 +147,18 @@ export function UsageDisplay({
tenant,
teamId,
keyAlias,
canEditBudget = false,
}: {
tenant?: string | null;
teamId?: string | null;
keyAlias?: string | null;
/**
* Feature 7: when true, the Budget StatCard becomes clickable and
* opens the budget editor. Off by default — owners and platform
* admins get it on; `user` role customers see the budget read-only.
* Server component decides this via canMutate(user).
*/
canEditBudget?: boolean;
}) {
const t = useTranslations("usage");
const [month, setMonth] = useState(getCurrentMonth);
@@ -185,11 +227,25 @@ export function UsageDisplay({
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard label={t("inputTokens")} value={fmt(data.currentPeriod.inputTokens)} />
<StatCard label={t("outputTokens")} value={fmt(data.currentPeriod.outputTokens)} />
<StatCard label={t("totalSpend")} value={usd(data.currentPeriod.totalSpend)} accent />
<StatCard
label={t("budget")}
value={data.budget.remaining !== null ? usd(data.budget.remaining) : t("noLimit")}
/>
<StatCard label={t("totalSpend")} value={chf(data.currentPeriod.totalSpend)} accent />
{canEditBudget && tenant ? (
<BudgetEditableCard
tenantName={tenant}
maxBudget={data.budget.maxBudget}
remaining={data.budget.remaining}
budgetDuration={data.budget.budgetDuration}
onSaved={fetchUsage}
/>
) : (
<StatCard
label={t("budget")}
value={
data.budget.remaining !== null
? chf(data.budget.remaining)
: t("noLimit")
}
/>
)}
</div>
<div className="bg-surface-1 border border-border rounded-xl p-5">