From 226188e22dcf20658120a5eaa9f3017322ac96e1 Mon Sep 17 00:00:00 2001 From: Evan Yang Date: Sat, 7 Mar 2026 13:25:26 +0800 Subject: [PATCH] Refine admin UI and models routing --- routstr/payment/models.py | 6 +- tests/unit/test_model_pricing_units.py | 9 +- ui/app/logs/log-filters.tsx | 7 +- ui/app/logs/page.tsx | 19 +-- ui/app/{model => models}/page.tsx | 0 ui/app/page.tsx | 14 ++ ui/app/providers/page.tsx | 37 +++-- ui/components/api-endpoint-tester.tsx | 5 +- ui/components/api-endpoint-types.ts | 2 +- ui/components/app-page-shell.tsx | 2 +- ui/components/app-sidebar.tsx | 2 +- ui/components/dashboard-balance-summary.tsx | 16 +-- ui/components/detailed-wallet-balance.tsx | 57 ++++---- ui/components/ecash-redeem.tsx | 7 +- ui/components/landing/api-key-manager.tsx | 5 +- .../landing/cashu-payment-workflow.tsx | 7 +- ui/components/landing/cheat-sheet.tsx | 12 +- ui/components/landing/key-info-details.tsx | 22 +-- .../landing/lightning-payment-workflow.tsx | 7 +- ui/components/model-provider-section.tsx | 4 +- ui/components/model-tester.tsx | 5 +- ui/components/provider-balance.tsx | 6 +- ui/components/provider-card.tsx | 20 ++- ui/components/site-header.tsx | 2 +- ui/components/temporary-balances.tsx | 128 ++++++++---------- ui/components/usage-summary-cards.tsx | 14 +- 26 files changed, 188 insertions(+), 227 deletions(-) rename ui/app/{model => models}/page.tsx (100%) diff --git a/routstr/payment/models.py b/routstr/payment/models.py index 5ae60d64..654891aa 100644 --- a/routstr/payment/models.py +++ b/routstr/payment/models.py @@ -72,14 +72,16 @@ def _normalize_legacy_prompt_completion_pricing( for field in ("prompt", "completion"): raw_value = normalized.get(field) + if not isinstance(raw_value, (str, int, float)): + continue try: - value = float(raw_value) if raw_value is not None else None + value = float(raw_value) except (TypeError, ValueError): continue # Older admin overrides were occasionally saved in "per 1M tokens" # units instead of the backend's expected per-token unit. - if value is not None and value > 0.1: + if value > 0.1: normalized[field] = value / 1_000_000 return normalized diff --git a/tests/unit/test_model_pricing_units.py b/tests/unit/test_model_pricing_units.py index 3ed05147..55ba1e79 100644 --- a/tests/unit/test_model_pricing_units.py +++ b/tests/unit/test_model_pricing_units.py @@ -1,6 +1,7 @@ """Tests for pricing unit normalization across model sources.""" import json +from types import TracebackType import pytest @@ -74,7 +75,13 @@ async def test_generic_provider_missing_pricing_defaults_to_zero( async def __aenter__(self) -> "FakeClient": return self - async def __aexit__(self, exc_type, exc, tb) -> None: + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + del exc_type, exc, tb return None async def get( diff --git a/ui/app/logs/log-filters.tsx b/ui/app/logs/log-filters.tsx index 38adc300..e9ce78d6 100644 --- a/ui/app/logs/log-filters.tsx +++ b/ui/app/logs/log-filters.tsx @@ -5,7 +5,7 @@ import { type KeyboardEvent, } from 'react'; import { format } from 'date-fns'; -import { CalendarIcon, Filter, X } from 'lucide-react'; +import { CalendarIcon, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, @@ -219,10 +219,7 @@ export function LogFilters({ return ( - - - Filters - + Filters Filter logs by date, level, request ID, text search, status code, method, endpoint and limit diff --git a/ui/app/logs/page.tsx b/ui/app/logs/page.tsx index b531116a..64d16c77 100644 --- a/ui/app/logs/page.tsx +++ b/ui/app/logs/page.tsx @@ -174,12 +174,7 @@ export default function LogsPage() { title='System Logs' description='View and filter application logs.' actions={ - @@ -208,21 +203,21 @@ export default function LogsPage() { - - Log Entries +
+ Log Entries {logsData && ( - + {logsData.logs.length} entries )} - +
{hasActiveFilters && ( - + Showing logs filtered by {activeFilterDescription} )}
- + {isLoading ? (
{Array.from({ length: 8 }).map((_, index) => ( diff --git a/ui/app/model/page.tsx b/ui/app/models/page.tsx similarity index 100% rename from ui/app/model/page.tsx rename to ui/app/models/page.tsx diff --git a/ui/app/page.tsx b/ui/app/page.tsx index cdc254e8..24ebfd7e 100644 --- a/ui/app/page.tsx +++ b/ui/app/page.tsx @@ -648,6 +648,20 @@ export default function DashboardPage() { } }, [chartConfigs, activeChartId]); + if (!isAuthResolved) { + return ( + +
+
+ + +
+ + +
+
+ ); + } if (!isAuthenticated) { return ; } diff --git a/ui/app/providers/page.tsx b/ui/app/providers/page.tsx index fb62f054..08906c55 100644 --- a/ui/app/providers/page.tsx +++ b/ui/app/providers/page.tsx @@ -20,6 +20,13 @@ import { Skeleton } from '@/components/ui/skeleton'; import { AlertCircle, Plus, Server } from 'lucide-react'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Dialog, DialogTrigger } from '@/components/ui/dialog'; +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from '@/components/ui/empty'; import { AlertDialog, AlertDialogAction, @@ -333,7 +340,7 @@ export default function ProvidersPage() { description='Manage your AI provider connections and credentials.' actions={ - @@ -376,18 +383,22 @@ export default function ProvidersPage() { ) : providers.length === 0 ? ( - - -

- No providers configured -

-

- Get started by adding your first upstream provider -

- + + + + + + + No providers configured + + Get started by adding your first upstream provider. + + + +
) : ( diff --git a/ui/components/api-endpoint-tester.tsx b/ui/components/api-endpoint-tester.tsx index e312bc28..a77b4248 100644 --- a/ui/components/api-endpoint-tester.tsx +++ b/ui/components/api-endpoint-tester.tsx @@ -483,10 +483,7 @@ export function ApiEndpointTester({ models }: ApiEndpointTesterProps) { return ( - - - API Endpoint Tester - + API Endpoint Tester Comprehensive testing of OpenAI-compatible API endpoints through the secure proxy (resolves CORS and network issues) diff --git a/ui/components/api-endpoint-types.ts b/ui/components/api-endpoint-types.ts index 9f4239ff..b97358e9 100644 --- a/ui/components/api-endpoint-types.ts +++ b/ui/components/api-endpoint-types.ts @@ -46,7 +46,7 @@ export const API_ENDPOINTS = { }, models: { name: 'List Models', - path: '/model', + path: '/models', icon: List, description: 'List all available models from the provider', }, diff --git a/ui/components/app-page-shell.tsx b/ui/components/app-page-shell.tsx index c4e02c48..73a7d8ac 100644 --- a/ui/components/app-page-shell.tsx +++ b/ui/components/app-page-shell.tsx @@ -40,7 +40,7 @@ const NAV_ITEMS = [ { title: 'Dashboard', url: '/', icon: LayoutDashboardIcon }, { title: 'Balances', url: '/balances', icon: WalletIcon }, { title: 'Logs', url: '/logs', icon: FileTextIcon }, - { title: 'Models', url: '/model', icon: DatabaseIcon }, + { title: 'Models', url: '/models', icon: DatabaseIcon }, { title: 'Providers', url: '/providers', icon: ServerIcon }, { title: 'Settings', url: '/settings', icon: SettingsIcon }, ] as const; diff --git a/ui/components/app-sidebar.tsx b/ui/components/app-sidebar.tsx index 11c56e69..76179f1c 100644 --- a/ui/components/app-sidebar.tsx +++ b/ui/components/app-sidebar.tsx @@ -52,7 +52,7 @@ const data = { }, { title: 'Models', - url: '/model', + url: '/models', icon: DatabaseIcon, }, { diff --git a/ui/components/dashboard-balance-summary.tsx b/ui/components/dashboard-balance-summary.tsx index c4c1f661..7f0ba782 100644 --- a/ui/components/dashboard-balance-summary.tsx +++ b/ui/components/dashboard-balance-summary.tsx @@ -78,23 +78,17 @@ export function DashboardBalanceSummary({ return (
{cards.map((card) => ( - + {card.title} -
- -
+ + +
-
+
{card.value}
diff --git a/ui/components/detailed-wallet-balance.tsx b/ui/components/detailed-wallet-balance.tsx index 631a42d2..dae15906 100644 --- a/ui/components/detailed-wallet-balance.tsx +++ b/ui/components/detailed-wallet-balance.tsx @@ -107,10 +107,15 @@ export function DetailedWalletBalance({ return ( <> - + -
- Cashu Wallet Balance +
+
+ Cashu Wallet Balance + + Detailed balance breakdown by mint and currency + +
- - Detailed balance breakdown by mint and currency - {isLoading ? (
{Array.from({ length: 3 }).map((_, index) => ( - + @@ -182,47 +180,47 @@ export function DetailedWalletBalance({ ) : (
- + - + Your Balance (Total) - - + + - -

+ +

{formatAmount(totals.totalOwner)}

- + - + Total Wallet - - + + - -

+ +

{formatAmount(totals.totalWallet)}

- + - + User Balance - - + + - -

+ +

{formatAmount(totals.totalUser)}

@@ -294,7 +292,6 @@ export function DetailedWalletBalance({ -
- - Send eCash -
+ Send eCash Generate a token to send eCash to someone diff --git a/ui/components/landing/api-key-manager.tsx b/ui/components/landing/api-key-manager.tsx index d14f173e..e8c2238e 100644 --- a/ui/components/landing/api-key-manager.tsx +++ b/ui/components/landing/api-key-manager.tsx @@ -172,10 +172,7 @@ export function ApiKeyManager({ return ( - - - API Key Management - + API Key Management

Manage your existing API keys and balances

diff --git a/ui/components/landing/cashu-payment-workflow.tsx b/ui/components/landing/cashu-payment-workflow.tsx index 8c09a660..d2e80c47 100644 --- a/ui/components/landing/cashu-payment-workflow.tsx +++ b/ui/components/landing/cashu-payment-workflow.tsx @@ -1,7 +1,7 @@ 'use client'; import { type JSX, useCallback, useState } from 'react'; -import { Copy, KeyRound, RefreshCcw, Trash2 } from 'lucide-react'; +import { Copy, RefreshCcw, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -305,10 +305,7 @@ export function CashuPaymentWorkflow({ return ( - - - API key workflow - + API key workflow

Sections expand as soon as you interact

diff --git a/ui/components/landing/cheat-sheet.tsx b/ui/components/landing/cheat-sheet.tsx index 68ed0d8a..7004ec98 100644 --- a/ui/components/landing/cheat-sheet.tsx +++ b/ui/components/landing/cheat-sheet.tsx @@ -2,7 +2,7 @@ import { type JSX, useCallback, useEffect, useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { Bolt, Copy, RefreshCcw, ShieldCheck, Terminal } from 'lucide-react'; +import { Bolt, Copy, RefreshCcw, Terminal } from 'lucide-react'; import { toast } from 'sonner'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -193,10 +193,7 @@ export function CheatSheet(): JSX.Element {
- - - Node identity - + Node identity

/v1/info snapshot

@@ -341,10 +338,7 @@ export function CheatSheet(): JSX.Element { - - - Quick docs - + Quick docs curl-ready diff --git a/ui/components/landing/key-info-details.tsx b/ui/components/landing/key-info-details.tsx index d9262a09..b65f50d0 100644 --- a/ui/components/landing/key-info-details.tsx +++ b/ui/components/landing/key-info-details.tsx @@ -4,11 +4,7 @@ import { type JSX, useState, useCallback, useEffect } from 'react'; import { Copy, RefreshCcw, - ShieldCheck, - History, - Users, RotateCcw, - KeyRound, } from 'lucide-react'; import { toast } from 'sonner'; import { @@ -154,10 +150,7 @@ export function KeyInfoDetails({
- - - Key Information - + Key Information Enter an API key to view its balance, consumption, and child keys. @@ -202,10 +195,7 @@ export function KeyInfoDetails({
- - - Status & Identity - + Status & Identity
@@ -255,10 +245,7 @@ export function KeyInfoDetails({ - - - Consumption - + Consumption
@@ -313,8 +300,7 @@ export function KeyInfoDetails({ walletInfo.childKeys.length > 0 && ( - - + Child Keys ({walletInfo.childKeys.length}) diff --git a/ui/components/landing/lightning-payment-workflow.tsx b/ui/components/landing/lightning-payment-workflow.tsx index 38111f87..9cc6152c 100644 --- a/ui/components/landing/lightning-payment-workflow.tsx +++ b/ui/components/landing/lightning-payment-workflow.tsx @@ -2,7 +2,7 @@ import { type JSX, useCallback, useState } from 'react'; import Image from 'next/image'; -import { Copy, Zap, CheckCircle, Loader2 } from 'lucide-react'; +import { Copy, CheckCircle, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import QRCode from 'qrcode'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -514,10 +514,7 @@ export function LightningPaymentWorkflow({ return ( - - - Lightning Payment Workflow - + Lightning Payment Workflow

Create and manage API keys using Lightning Network payments

diff --git a/ui/components/model-provider-section.tsx b/ui/components/model-provider-section.tsx index fdf7a15c..a29ecf1a 100644 --- a/ui/components/model-provider-section.tsx +++ b/ui/components/model-provider-section.tsx @@ -25,7 +25,6 @@ import { Key, MoreVertical, RefreshCw, - Users, } from 'lucide-react'; interface ModelProviderSectionProps { @@ -133,8 +132,7 @@ export function ModelProviderSection({ aria-label={`Select models for provider ${provider}`} />
- - + {provider} ({providerModels.length} models) diff --git a/ui/components/model-tester.tsx b/ui/components/model-tester.tsx index d978422d..dee500a7 100644 --- a/ui/components/model-tester.tsx +++ b/ui/components/model-tester.tsx @@ -203,10 +203,7 @@ export function ModelTester({ models }: ModelTesterProps) { return ( - - - Model Credential Tester - + Model Credential Tester Test model functionality by sending chat completion requests through the secure proxy (resolves CORS and network issues) diff --git a/ui/components/provider-balance.tsx b/ui/components/provider-balance.tsx index 93f26ff8..b6460d97 100644 --- a/ui/components/provider-balance.tsx +++ b/ui/components/provider-balance.tsx @@ -173,9 +173,11 @@ export function ProviderBalance({ onClick={handleTopUpClick} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} - className='w-full font-mono sm:w-auto' + className='w-full sm:w-auto' > - {isHovered ? 'Top Up' : displayValue} + + {isHovered ? 'Top Up' : displayValue} + diff --git a/ui/components/provider-card.tsx b/ui/components/provider-card.tsx index 74f502ce..bd736d3c 100644 --- a/ui/components/provider-card.tsx +++ b/ui/components/provider-card.tsx @@ -54,22 +54,20 @@ export function ProviderCard({ const hasDetails = Boolean(provider.api_version) || isExpanded; return ( - - + +
- - {provider.provider_type} - + {provider.provider_type} {provider.enabled ? 'Enabled' : 'Disabled'}
- + {provider.base_url}
@@ -88,7 +86,7 @@ export function ProviderCard({ variant='outline' size='sm' onClick={onToggleExpansion} - className='col-span-2 h-9 justify-between px-3 sm:col-auto sm:h-8 sm:justify-center' + className='col-span-2 justify-between sm:col-auto sm:justify-center' > @@ -105,7 +103,7 @@ export function ProviderCard({ variant='outline' size='sm' onClick={onEditProvider} - className='h-9 justify-center gap-1.5 px-3 sm:h-8' + className='justify-center gap-1.5' > Edit @@ -115,7 +113,7 @@ export function ProviderCard({ variant='outline' size='sm' onClick={onDeleteProvider} - className='text-destructive hover:text-destructive h-9 justify-center gap-1.5 px-3 sm:h-8' + className='text-destructive hover:text-destructive justify-center gap-1.5' > Delete @@ -125,7 +123,7 @@ export function ProviderCard({ {hasDetails ? ( - +
{provider.api_version && (
diff --git a/ui/components/site-header.tsx b/ui/components/site-header.tsx index ffef86ba..86cc343e 100644 --- a/ui/components/site-header.tsx +++ b/ui/components/site-header.tsx @@ -23,7 +23,7 @@ const PAGE_META: Record = { title: 'System Logs', description: 'Inspect request and application logs.', }, - '/model': { + '/models': { title: 'Models', description: 'Manage model catalog and provider mappings.', }, diff --git a/ui/components/temporary-balances.tsx b/ui/components/temporary-balances.tsx index c3bcbb75..3ab53354 100644 --- a/ui/components/temporary-balances.tsx +++ b/ui/components/temporary-balances.tsx @@ -9,7 +9,6 @@ import { Clock, DollarSign, Activity, - type LucideIcon, } from 'lucide-react'; import { AdminService, TemporaryBalance } from '@/lib/api/services/admin'; import { @@ -43,39 +42,6 @@ import { cn } from '@/lib/utils'; import type { DisplayUnit } from '@/lib/types/units'; import { formatFromMsat } from '@/lib/currency'; -function TemporaryBalanceStat({ - icon: Icon, - label, - value, - iconClassName, -}: { - icon: LucideIcon; - label: string; - value: string; - iconClassName: string; -}) { - return ( - - - {label} - - - - - -

- {value} -

-
-
- ); -} - function getTotals(balances: TemporaryBalance[]) { let totalBalance = 0; let totalSpent = 0; @@ -157,32 +123,32 @@ export function TemporaryBalances({ const rows = data ? buildHierarchicalData(data, filteredData) : []; return ( - + -
- - - Temporary Balances - -
-
+
+
+ Temporary Balances + + API keys with their current balances and usage statistics + +
+ +
+
setSearchTerm(e.target.value)} - className='pr-3 pl-8 sm:w-64' name='temporary_balance_search' autoComplete='off' /> -
- - API keys with their current balances and usage statistics - {isLoading ? (
{Array.from({ length: 3 }).map((_, index) => ( - + @@ -236,24 +196,51 @@ export function TemporaryBalances({ ) : (
- - - + + + + Total Balance + + + + + + +

+ {formatBalance(totals.totalBalance)} +

+
+
+ + + + Total Spent + + + + + + +

+ {formatBalance(totals.totalSpent)} +

+
+
+ + + + Total Requests + + + + + + +

+ {totals.totalRequests.toLocaleString()} +

+
+
{rows.length > 0 ? ( @@ -343,7 +330,6 @@ export function TemporaryBalances({ {cards.map((card) => ( - - - + + + {card.title} - - + + -
+
{card.value}