import { Fragment } from "react"; import { Icon } from "./Icon"; import { useFetch } from "../hooks"; import { fmtCost, fmtPercent, useT } from "../i18n"; interface BudgetRow { provider: string; budget: number | null; spent: number; remaining: number | null; share: number | null; warning: boolean; exceeded: boolean; images?: boolean; plan?: Plan; remote?: { currency?: string; remaining?: number | null; limit?: number | null; used?: number | null; spent_month?: number; scope?: string; error?: string }; } /** opencode Go: usage windows against the plan's limits (USD). */ export interface Plan { limit: number; windows: { window: "5h" | "week" | "month"; used: number; limit: number; ratio: number }[]; } /** Three small meters (5 hours, week, month) for a subscription plan. */ export function PlanUsage({ plan }: { plan: Plan }) { const t = useT(); return (
{plan.windows.map((w) => (
{t(`budget.window.${w.window}`)} = 0.8} exceeded={w.ratio >= 1} /> {fmtPercent(w.ratio)}
))}
); } interface BudgetData { currency: string; total: BudgetRow; providers: BudgetRow[]; } export function Bar({ share, warning, exceeded }: { share: number; warning: boolean; exceeded: boolean }) { const color = exceeded ? "var(--red)" : warning ? "var(--accent2)" : "var(--green)"; return (
); } function Remote({ r, t }: { r: BudgetRow["remote"]; t: ReturnType }) { if (!r) return –; if (r.error) return {t("budget.remoteError")}; const cur = r.currency ?? "USD"; if (r.spent_month !== undefined) return {t("budget.providerSpent", { v: fmtCost(r.spent_month, cur) })}; if (r.remaining !== null && r.remaining !== undefined) return {t("budget.balance", { v: fmtCost(r.remaining, cur) })}; if (r.used !== null && r.used !== undefined) return {t("budget.providerSpent", { v: fmtCost(r.used, cur) })}; return –; } /** Monthly budgets per provider (admins). */ export function BudgetPanel() { const t = useT(); const { data, error } = useFetch("/budget"); if (error || !data) return null; const rows = [data.total, ...data.providers]; return (

{t("budget.title")}

{rows.map((r) => ( {r.plan && ( )} ))}
{t("stats.provider")} {t("budget.spent")} {t("budget.budget")} {t("budget.remaining")} {t("budget.provider")}
{r.provider === "total" ? {t("stats.total")} : r.provider} {r.images && {t("nav.images")}} {fmtCost(r.spent, data.currency)} {r.budget ? fmtCost(r.budget, data.currency) : "–"} {r.share !== null && } {r.remaining !== null ? fmtCost(r.remaining, data.currency) : "–"} {r.share !== null && ({fmtPercent(r.share)})} {r.provider === "total" ? null : }
{t("budget.plan", { v: fmtCost(r.plan.limit, "USD") })}
{t("budget.hint")}
); } /** Small header chip when the overall budget is nearly used up. */ export function BudgetChip() { const t = useT(); const { data } = useFetch("/budget"); const worst = data ? [data.total, ...data.providers].filter((r) => r.warning).sort((a, b) => (b.share ?? 0) - (a.share ?? 0))[0] : undefined; if (!worst) return null; return ( {t("budget.chip", { name: worst.provider === "total" ? t("stats.total") : worst.provider, p: fmtPercent(worst.share ?? 0) })} ); }