From 82e01e3e138f4ccb8a2e83eb3a5d5c33b70bcbac Mon Sep 17 00:00:00 2001
From: 9qeklajc <9qeklajc>
Date: Fri, 24 Oct 2025 21:31:35 +0200
Subject: [PATCH] add temp balances
---
routstr/core/admin.py | 19 ++
ui/app/page.tsx | 4 +
ui/components/ModelSelector.tsx | 1 -
ui/components/temporary-balances.tsx | 248 +++++++++++++++++++++++++++
ui/lib/api/services/admin.ts | 17 ++
5 files changed, 288 insertions(+), 1 deletion(-)
create mode 100644 ui/components/temporary-balances.tsx
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)}
+
+
+
+
+
+ {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;