mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
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:
77
src/app/(dashboard)/dashboard/costs/page.js
Normal file
77
src/app/(dashboard)/dashboard/costs/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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" && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user