feat: add costs page and enhance health provider status display

Add dedicated Costs page combining Budget and Pricing tabs with a
sidebar navigation layout. Improve Health page provider section with
status legend (Healthy/Recovering/Down) and import AI_PROVIDERS
constants for richer provider display.
This commit is contained in:
diegosouzapw
2026-02-15 23:01:27 -03:00
parent e4db9bff0e
commit 9f8dfa1398
6 changed files with 194 additions and 39 deletions

View File

@@ -0,0 +1,77 @@
"use client";
import { useState } from "react";
import { cn } from "@/shared/utils/cn";
import BudgetTab from "../usage/components/BudgetTab";
import PricingTab from "../settings/components/PricingTab";
const sections = [
{
id: "budget",
label: "Budget",
icon: "account_balance_wallet",
description: "Daily and monthly spend limits",
},
{
id: "pricing",
label: "Pricing",
icon: "payments",
description: "Per-model cost configuration",
},
];
export default function CostsPage() {
const [activeSection, setActiveSection] = useState("budget");
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div>
<h1 className="text-2xl font-semibold">Costs</h1>
<p className="text-sm text-text-muted mt-1">
Budget limits and model pricing configuration
</p>
</div>
{/* Layout: sidebar + content */}
<div className="flex gap-6">
{/* Sidebar */}
<nav className="shrink-0 w-48">
<div className="flex flex-col gap-1 sticky top-4">
{sections.map((section) => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={cn(
"flex items-center gap-2.5 px-3 py-2.5 rounded-lg text-sm font-medium transition-all text-left w-full",
activeSection === section.id
? "bg-primary/10 text-primary shadow-sm"
: "text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5"
)}
>
<span className="material-symbols-outlined text-[18px]">{section.icon}</span>
<div className="min-w-0">
<div>{section.label}</div>
<div
className={cn(
"text-[10px] font-normal truncate",
activeSection === section.id ? "text-primary/70" : "text-text-muted"
)}
>
{section.description}
</div>
</div>
</button>
))}
</div>
</nav>
{/* Content */}
<div className="flex-1 min-w-0">
{activeSection === "budget" && <BudgetTab />}
{activeSection === "pricing" && <PricingTab />}
</div>
</div>
</div>
);
}

View File

@@ -14,6 +14,7 @@
import { useState, useEffect, useCallback } from "react";
import { Card } from "@/shared/components";
import { AI_PROVIDERS } from "@/shared/constants/providers";
function formatUptime(seconds) {
const d = Math.floor(seconds / 86400);
@@ -324,38 +325,120 @@ export default function HealthPage() {
{/* Provider Health */}
<Card className="p-5" role="region" aria-label="Provider health status">
<h2 className="text-lg font-semibold text-text-main mb-4 flex items-center gap-2">
<span className="material-symbols-outlined text-[20px] text-primary">
health_and_safety
</span>
Provider Health
</h2>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-text-main flex items-center gap-2">
<span className="material-symbols-outlined text-[20px] text-primary">
health_and_safety
</span>
Provider Health
</h2>
{cbEntries.length > 0 && (
<div className="flex items-center gap-3 text-xs text-text-muted">
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-green-500" /> Healthy
</span>
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-amber-500" /> Recovering
</span>
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-red-500" /> Down
</span>
</div>
)}
</div>
{cbEntries.length === 0 ? (
<p className="text-sm text-text-muted text-center py-4">
No circuit breaker data available. Make some requests first.
</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{cbEntries.map(([provider, cb]) => {
const style = CB_COLORS[cb.state] || CB_COLORS.CLOSED;
return (
<div key={provider} className={`rounded-lg p-3 ${style.bg} border border-white/5`}>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium text-text-main">{provider}</span>
<span className={`text-xs font-semibold ${style.text}`}>{style.label}</span>
(() => {
const unhealthy = cbEntries.filter(([, cb]) => cb.state !== "CLOSED");
const healthy = cbEntries.filter(([, cb]) => cb.state === "CLOSED");
return (
<div className="space-y-4">
{/* Unhealthy providers first */}
{unhealthy.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-red-400 uppercase tracking-wide">
Issues Detected
</p>
{unhealthy.map(([provider, cb]) => {
const style = CB_COLORS[cb.state] || CB_COLORS.OPEN;
const providerInfo = AI_PROVIDERS[provider];
const displayName = providerInfo?.name || provider;
return (
<div
key={provider}
className={`rounded-lg p-3 ${style.bg} border border-white/5 flex items-center gap-3`}
>
<div
className="size-8 rounded-lg flex items-center justify-center shrink-0 text-xs font-bold"
style={{
backgroundColor: `${providerInfo?.color || "#888"}15`,
color: providerInfo?.color || "#888",
}}
>
{providerInfo?.textIcon || provider.slice(0, 2).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-text-main truncate">
{displayName}
</span>
<span
className={`text-xs font-semibold px-1.5 py-0.5 rounded ${style.bg} ${style.text}`}
>
{style.label}
</span>
</div>
<div className="text-xs text-text-muted mt-0.5">
{cb.failures} failure{cb.failures !== 1 ? "s" : ""}
{cb.lastFailure && (
<span className="ml-2">
· Last: {new Date(cb.lastFailure).toLocaleTimeString()}
</span>
)}
</div>
</div>
</div>
);
})}
</div>
<div className="text-xs text-text-muted">
Failures: {cb.failures || 0}
{cb.lastFailure && (
<span className="ml-2">
Last: {new Date(cb.lastFailure).toLocaleTimeString()}
</span>
)}
{/* Healthy providers in compact grid */}
{healthy.length > 0 && (
<div>
{unhealthy.length > 0 && (
<p className="text-xs font-medium text-green-400 uppercase tracking-wide mb-2">
Operational
</p>
)}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
{healthy.map(([provider]) => {
const providerInfo = AI_PROVIDERS[provider];
const displayName = providerInfo?.name || provider;
return (
<div
key={provider}
className="rounded-lg p-2.5 bg-green-500/5 border border-white/5 flex items-center gap-2"
>
<span className="size-2 rounded-full bg-green-500 shrink-0" />
<span
className="text-xs font-medium text-text-main truncate"
title={displayName}
>
{displayName}
</span>
</div>
);
})}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
);
})()
)}
</Card>

View File

@@ -11,7 +11,6 @@ import ProxyTab from "./components/ProxyTab";
import AppearanceTab from "./components/AppearanceTab";
import ThinkingBudgetTab from "./components/ThinkingBudgetTab";
import SystemPromptTab from "./components/SystemPromptTab";
import PricingTab from "./components/PricingTab";
import ComplianceTab from "./components/ComplianceTab";
import CacheStatsCard from "./components/CacheStatsCard";
import ResilienceTab from "./components/ResilienceTab";
@@ -22,7 +21,6 @@ const tabs = [
{ id: "security", label: "Security", icon: "shield" },
{ id: "routing", label: "Routing", icon: "route" },
{ id: "resilience", label: "Resilience", icon: "electrical_services" },
{ id: "pricing", label: "Pricing", icon: "payments" },
{ id: "advanced", label: "Advanced", icon: "tune" },
{ id: "compliance", label: "Compliance", icon: "policy" },
];
@@ -88,8 +86,6 @@ export default function SettingsPage() {
</div>
)}
{activeTab === "pricing" && <PricingTab />}
{activeTab === "resilience" && <ResilienceTab />}
{activeTab === "advanced" && (

View File

@@ -5,19 +5,17 @@ import { RequestLoggerV2, ProxyLogger, CardSkeleton, SegmentedControl } from "@/
import ProviderLimits from "./components/ProviderLimits";
import SessionsTab from "./components/SessionsTab";
import RateLimitStatus from "./components/RateLimitStatus";
import BudgetTab from "./components/BudgetTab";
export default function UsagePage() {
const [activeTab, setActiveTab] = useState("limits");
const [activeTab, setActiveTab] = useState("logs");
return (
<div className="flex flex-col gap-6">
<SegmentedControl
options={[
{ value: "limits", label: "Limits" },
{ value: "logs", label: "Logger" },
{ value: "proxy-logs", label: "Proxy" },
{ value: "budget", label: "Budget" },
{ value: "limits", label: "Limits" },
]}
value={activeTab}
onChange={setActiveTab}
@@ -35,7 +33,6 @@ export default function UsagePage() {
)}
{activeTab === "logs" && <RequestLoggerV2 />}
{activeTab === "proxy-logs" && <ProxyLogger />}
{activeTab === "budget" && <BudgetTab />}
</div>
);
}

View File

@@ -31,14 +31,15 @@ export async function GET() {
platform: process.platform,
};
// Provider health summary
// Provider health summary (circuitBreakers is an Array of { name, state, ... })
const providerHealth = {};
for (const [key, cb] of Object.entries(circuitBreakers)) {
providerHealth[key] = {
for (const cb of circuitBreakers) {
// Skip test circuit breakers (leftover from unit tests)
if (cb.name.startsWith("test-") || cb.name.startsWith("test_")) continue;
providerHealth[cb.name] = {
state: cb.state,
failures: cb.failures,
lastFailure: cb.lastFailure,
nextRetry: cb.nextRetry,
failures: cb.failureCount || 0,
lastFailure: cb.lastFailureTime,
};
}

View File

@@ -17,6 +17,7 @@ const navItems = [
{ href: "/dashboard/providers", label: "Providers", icon: "dns" },
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
{ href: "/dashboard/costs", label: "Costs", icon: "account_balance_wallet" },
{ href: "/dashboard/analytics", label: "Analytics", icon: "analytics" },
{ href: "/dashboard/health", label: "Health", icon: "health_and_safety" },
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },