diff --git a/routstr/core/admin.py b/routstr/core/admin.py index f2ca7134..3e7bb8c7 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -147,6 +147,25 @@ async def partial_apikeys(request: Request) -> str: """ +@admin_router.get("/api/temporary-balances", dependencies=[Depends(require_admin_api)]) +async def get_temporary_balances_api(request: Request) -> list[dict[str, object]]: + async with create_session() as session: + result = await session.exec(select(ApiKey)) + api_keys = result.all() + + return [ + { + "hashed_key": key.hashed_key, + "balance": key.balance, + "total_spent": key.total_spent, + "total_requests": key.total_requests, + "refund_address": key.refund_address, + "key_expiry_time": key.key_expiry_time, + } + for key in api_keys + ] + + @admin_router.get("/api/balances", dependencies=[Depends(require_admin_api)]) async def get_balances_api(request: Request) -> list[dict[str, object]]: balance_details, _tw, _tu, _ow = await fetch_all_balances() diff --git a/ui/app/page.tsx b/ui/app/page.tsx index fde61885..cfe1c3f7 100644 --- a/ui/app/page.tsx +++ b/ui/app/page.tsx @@ -2,6 +2,7 @@ import { AppSidebar } from '@/components/app-sidebar'; import { SiteHeader } from '@/components/site-header'; import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; import { DetailedWalletBalance } from '@/components/detailed-wallet-balance'; +import { TemporaryBalances } from '@/components/temporary-balances'; export default function Page() { return ( @@ -23,6 +24,9 @@ export default function Page() {
+
+ +
diff --git a/ui/components/ModelSelector.tsx b/ui/components/ModelSelector.tsx index 49210504..62fe0ea1 100644 --- a/ui/components/ModelSelector.tsx +++ b/ui/components/ModelSelector.tsx @@ -42,7 +42,6 @@ import { DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; import { - Check, Cpu, Zap, Edit3, diff --git a/ui/components/temporary-balances.tsx b/ui/components/temporary-balances.tsx new file mode 100644 index 00000000..50aa497e --- /dev/null +++ b/ui/components/temporary-balances.tsx @@ -0,0 +1,248 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { + Loader2, + RefreshCw, + AlertCircle, + Key, + Clock, + DollarSign, + Activity, +} from 'lucide-react'; +import { AdminService, TemporaryBalance } from '@/lib/api/services/admin'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +export function TemporaryBalances({ + refreshInterval = 10000, +}: { + refreshInterval?: number; +}) { + const [searchTerm, setSearchTerm] = useState(''); + + const { data, isLoading, isError, error, isFetching, refetch } = useQuery({ + queryKey: ['temporary-balances'], + queryFn: async () => { + return AdminService.getTemporaryBalances(); + }, + refetchInterval: refreshInterval, + }); + + const formatBalance = (balance: number) => { + return `${balance.toLocaleString()} mSats`; + }; + + const filteredData = data + ? data.filter( + (item) => + item.hashed_key.toLowerCase().includes(searchTerm.toLowerCase()) || + item.refund_address?.toLowerCase().includes(searchTerm.toLowerCase()) + ) + : []; + + const calculateTotals = (balances: TemporaryBalance[]) => { + let totalBalance = 0; + let totalSpent = 0; + let totalRequests = 0; + + balances.forEach((balance) => { + totalBalance += balance.balance || 0; + totalSpent += balance.total_spent || 0; + totalRequests += balance.total_requests || 0; + }); + + return { totalBalance, totalSpent, totalRequests }; + }; + + const totals = data + ? calculateTotals(data) + : { totalBalance: 0, totalSpent: 0, totalRequests: 0 }; + + return ( + <> + + +
+ + + Temporary Balances + +
+
+ setSearchTerm(e.target.value)} + className='focus:ring-primary/20 w-64 rounded-md border py-2 pr-3 pl-8 text-sm focus:ring-2 focus:outline-none' + /> + +
+ +
+
+ + API keys with their current balances and usage statistics + +
+ + {isLoading ? ( +
+ +
+ ) : isError ? ( +
+ + + Error loading temporary balances: {(error as Error).message} + +
+ ) : ( +
+ {/* Summary Cards */} +
+
+
+
+ + + Total Balance + +
+
+
+ {formatBalance(totals.totalBalance)} +
+
+
+
+
+ + + Total Spent + +
+
+
+ {formatBalance(totals.totalSpent)} +
+
+
+
+
+ + + Total Requests + +
+
+
+ {totals.totalRequests.toLocaleString()} +
+
+
+ + {/* Table */} +
+
+
Hashed Key
+
Balance
+
Total Spent
+
Total Requests
+
Refund Address
+
Expiry Time
+
+ + {filteredData.length > 0 ? ( + filteredData.map((balance, index) => ( +
+
+ {balance.hashed_key} +
+
+ {formatBalance(balance.balance)} +
+
+ {formatBalance(balance.total_spent)} +
+
+ {balance.total_requests.toLocaleString()} +
+
+ {balance.refund_address || '-'} +
+
+ {balance.key_expiry_time ? ( +
+ + + {new Date( + balance.key_expiry_time * 1000 + ).toLocaleDateString()} + +
+ ) : ( + '-' + )} +
+
+ )) + ) : ( +
+ {searchTerm ? ( +
+ + No temporary balances match your search +
+ ) : ( +
+ + No temporary balances found +
+ )} +
+ )} +
+ + {data && data.length > 0 && ( +
+ Showing {filteredData.length} of {data.length} temporary + balances +
+ )} +
+ )} +
+
+ + ); +} diff --git a/ui/lib/api/services/admin.ts b/ui/lib/api/services/admin.ts index 8a80e92d..e4b64124 100644 --- a/ui/lib/api/services/admin.ts +++ b/ui/lib/api/services/admin.ts @@ -783,4 +783,21 @@ export class AdminService { static async logout(): Promise<{ ok: boolean }> { return await apiClient.post<{ ok: boolean }>('/admin/api/logout', {}); } + + static async getTemporaryBalances(): Promise { + return await apiClient.get( + '/admin/api/temporary-balances' + ); + } } + +export const TemporaryBalanceSchema = z.object({ + hashed_key: z.string(), + balance: z.number(), + total_spent: z.number(), + total_requests: z.number(), + refund_address: z.string().nullable(), + key_expiry_time: z.number().nullable(), +}); + +export type TemporaryBalance = z.infer;