make format required & fix ui formating

This commit is contained in:
9qeklajc
2025-12-10 22:51:12 +01:00
parent 02ca36141e
commit 7ca69063bd
18 changed files with 378 additions and 216 deletions
+6 -2
View File
@@ -62,14 +62,18 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: "18"
cache: 'npm' cache: "npm"
cache-dependency-path: ui/package-lock.json cache-dependency-path: ui/package-lock.json
- name: Install UI dependencies - name: Install UI dependencies
working-directory: ./ui working-directory: ./ui
run: npm ci run: npm ci
- name: Run UI format check
working-directory: ./ui
run: npm run format-check
- name: Run UI linting - name: Run UI linting
working-directory: ./ui working-directory: ./ui
run: npm run lint run: npm run lint
+1 -3
View File
@@ -29,9 +29,7 @@ export default function BalancesPage() {
<div className='container max-w-6xl px-4 py-8 md:px-6 lg:px-8'> <div className='container max-w-6xl px-4 py-8 md:px-6 lg:px-8'>
<div className='mb-8 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'> <div className='mb-8 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'>
<div> <div>
<h1 className='text-3xl font-bold tracking-tight'> <h1 className='text-3xl font-bold tracking-tight'>Balances</h1>
Balances
</h1>
<p className='text-muted-foreground mt-2'> <p className='text-muted-foreground mt-2'>
Monitor and manage wallet balances Monitor and manage wallet balances
</p> </p>
+2 -2
View File
@@ -78,8 +78,8 @@ export default function AdminLoginPage(): ReactElement {
}; };
return ( return (
<div className='flex min-h-screen items-center justify-center bg-background px-4 py-12 text-foreground'> <div className='bg-background text-foreground flex min-h-screen items-center justify-center px-4 py-12'>
<Card className='w-full max-w-md border border-border/60 bg-card/90 shadow-2xl shadow-black/30 backdrop-blur'> <Card className='border-border/60 bg-card/90 w-full max-w-md border shadow-2xl shadow-black/30 backdrop-blur'>
<CardHeader className='space-y-1'> <CardHeader className='space-y-1'>
<CardTitle className='text-center text-2xl font-bold'> <CardTitle className='text-center text-2xl font-bold'>
Admin Login Admin Login
+10 -3
View File
@@ -97,7 +97,7 @@ export function LogDetailsDialog({
<div> <div>
<h4 className='mb-2 text-sm font-medium'>Message</h4> <h4 className='mb-2 text-sm font-medium'>Message</h4>
<div className='bg-muted max-h-48 overflow-auto rounded-md p-3'> <div className='bg-muted max-h-48 overflow-auto rounded-md p-3'>
<pre className='font-mono text-sm whitespace-pre break-all'> <pre className='font-mono text-sm break-all whitespace-pre'>
{log.message} {log.message}
</pre> </pre>
</div> </div>
@@ -116,7 +116,12 @@ export function LogDetailsDialog({
<Button <Button
variant='outline' variant='outline'
size='sm' size='sm'
onClick={() => copyToClipboard(String(log[field as keyof LogEntry] || ''), field)} onClick={() =>
copyToClipboard(
String(log[field as keyof LogEntry] || ''),
field
)
}
className='h-6 flex-shrink-0 px-2' className='h-6 flex-shrink-0 px-2'
> >
{copiedField === field ? ( {copiedField === field ? (
@@ -175,7 +180,9 @@ export function LogDetailsDialog({
<Button <Button
variant='outline' variant='outline'
size='sm' size='sm'
onClick={() => copyToClipboard(JSON.stringify(log, null, 2), 'json')} onClick={() =>
copyToClipboard(JSON.stringify(log, null, 2), 'json')
}
className='h-6 px-2' className='h-6 px-2'
> >
{copiedField === 'json' ? ( {copiedField === 'json' ? (
+23 -10
View File
@@ -205,32 +205,45 @@ export default function ModelsPage() {
</div> </div>
</div> </div>
{totalModels === 0 && ( {totalModels === 0 && (
<Alert className="mb-4"> <Alert className='mb-4'>
<AlertCircle className='h-4 w-4' /> <AlertCircle className='h-4 w-4' />
<AlertDescription> <AlertDescription>
<div className='space-y-2'> <div className='space-y-2'>
<p className='font-medium'> <p className='font-medium'>
No models found for this provider No models found for this provider
</p> </p>
<div className='text-sm space-y-1'> <div className='space-y-1 text-sm'>
<p className='font-medium'>Common issues:</p> <p className='font-medium'>
<ul className='list-disc list-inside space-y-1 ml-2'> Common issues:
</p>
<ul className='ml-2 list-inside list-disc space-y-1'>
<li> <li>
<strong>API credentials:</strong> Check if the API key is correct and has the right permissions <strong>API credentials:</strong>{' '}
Check if the API key is correct
and has the right permissions
</li> </li>
<li> <li>
<strong>Base URL:</strong> Verify the base URL is correct for your provider <strong>Base URL:</strong> Verify
the base URL is correct for your
provider
</li> </li>
<li> <li>
<strong>Network access:</strong> Ensure the server can reach the provider&apos;s API endpoint <strong>Network access:</strong>{' '}
Ensure the server can reach the
provider&apos;s API endpoint
</li> </li>
<li> <li>
<strong>Provider status:</strong> The upstream provider might be temporarily unavailable <strong>Provider status:</strong>{' '}
The upstream provider might be
temporarily unavailable
</li> </li>
</ul> </ul>
{groupData?.group_url && ( {groupData?.group_url && (
<p className='mt-2 text-xs text-muted-foreground'> <p className='text-muted-foreground mt-2 text-xs'>
Current endpoint: <code className='bg-muted px-1 rounded'>{groupData.group_url}</code> Current endpoint:{' '}
<code className='bg-muted rounded px-1'>
{groupData.group_url}
</code>
</p> </p>
)} )}
</div> </div>
+49 -23
View File
@@ -53,7 +53,7 @@ export default function DashboardPage() {
window.removeEventListener('storage', syncAuthState); window.removeEventListener('storage', syncAuthState);
}; };
}, []); }, []);
const { data: btcUsdPrice } = useQuery({ const { data: btcUsdPrice } = useQuery({
queryKey: ['btc-usd-price'], queryKey: ['btc-usd-price'],
queryFn: fetchBtcUsdPrice, queryFn: fetchBtcUsdPrice,
@@ -131,8 +131,13 @@ export default function DashboardPage() {
<SiteHeader /> <SiteHeader />
<div className='container max-w-7xl px-4 py-8 md:px-6 lg:px-8'> <div className='container max-w-7xl px-4 py-8 md:px-6 lg:px-8'>
<div className='mb-8'> <div className='mb-8'>
<h1 className='text-3xl font-bold tracking-tight mb-6'>Dashboard</h1> <h1 className='mb-6 text-3xl font-bold tracking-tight'>
<DashboardBalanceSummary displayUnit={displayUnit} usdPerSat={usdPerSat} /> Dashboard
</h1>
<DashboardBalanceSummary
displayUnit={displayUnit}
usdPerSat={usdPerSat}
/>
</div> </div>
<div className='mb-6 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between'> <div className='mb-6 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between'>
@@ -140,8 +145,9 @@ export default function DashboardPage() {
<h2 className='text-xl font-semibold tracking-tight'> <h2 className='text-xl font-semibold tracking-tight'>
Usage Analytics Usage Analytics
</h2> </h2>
<p className='text-muted-foreground text-sm mt-1'> <p className='text-muted-foreground mt-1 text-sm'>
Monitor requests, errors, and revenue over the last {timeRange} hours Monitor requests, errors, and revenue over the last {timeRange}{' '}
hours
</p> </p>
</div> </div>
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
@@ -176,27 +182,31 @@ export default function DashboardPage() {
<div className='space-y-6'> <div className='space-y-6'>
{summaryLoading ? ( {summaryLoading ? (
<div className='text-center py-8'>Loading summary...</div> <div className='py-8 text-center'>Loading summary...</div>
) : summaryData ? ( ) : summaryData ? (
<UsageSummaryCards summary={summaryData} /> <UsageSummaryCards summary={summaryData} />
) : null} ) : null}
<div className='grid gap-6 lg:grid-cols-2'> <div className='grid gap-6 lg:grid-cols-2'>
{metricsLoading ? ( {metricsLoading ? (
<div className='text-center py-8 col-span-2'> <div className='col-span-2 py-8 text-center'>
Loading metrics... Loading metrics...
</div> </div>
) : metricsData && metricsData.metrics.length > 0 ? ( ) : metricsData && metricsData.metrics.length > 0 ? (
<> <>
<div className="col-span-full"> <div className='col-span-full'>
<UsageMetricsChart <UsageMetricsChart
data={metricsData.metrics.map((m) => ({ data={
...m, metricsData.metrics.map((m) => ({
revenue_sats: m.revenue_msats / 1000, ...m,
refunds_sats: m.refunds_msats / 1000, revenue_sats: m.revenue_msats / 1000,
net_revenue_sats: refunds_sats: m.refunds_msats / 1000,
(m.revenue_msats - m.refunds_msats) / 1000, net_revenue_sats:
})) as Array<Record<string, unknown> & { timestamp: string }>} (m.revenue_msats - m.refunds_msats) / 1000,
})) as Array<
Record<string, unknown> & { timestamp: string }
>
}
title='Revenue Over Time (sats)' title='Revenue Over Time (sats)'
dataKeys={[ dataKeys={[
{ {
@@ -218,7 +228,11 @@ export default function DashboardPage() {
/> />
</div> </div>
<UsageMetricsChart <UsageMetricsChart
data={metricsData.metrics as Array<Record<string, unknown> & { timestamp: string }>} data={
metricsData.metrics as Array<
Record<string, unknown> & { timestamp: string }
>
}
title='Request Volume' title='Request Volume'
dataKeys={[ dataKeys={[
{ {
@@ -239,7 +253,11 @@ export default function DashboardPage() {
]} ]}
/> />
<UsageMetricsChart <UsageMetricsChart
data={metricsData.metrics as Array<Record<string, unknown> & { timestamp: string }>} data={
metricsData.metrics as Array<
Record<string, unknown> & { timestamp: string }
>
}
title='Error Tracking' title='Error Tracking'
dataKeys={[ dataKeys={[
{ {
@@ -260,7 +278,11 @@ export default function DashboardPage() {
]} ]}
/> />
<UsageMetricsChart <UsageMetricsChart
data={metricsData.metrics as Array<Record<string, unknown> & { timestamp: string }>} data={
metricsData.metrics as Array<
Record<string, unknown> & { timestamp: string }
>
}
title='Payment Activity' title='Payment Activity'
dataKeys={[ dataKeys={[
{ {
@@ -270,7 +292,7 @@ export default function DashboardPage() {
}, },
]} ]}
/> />
<div className="space-y-6"> <div className='space-y-6'>
{summaryData && summaryData.unique_models.length > 0 && ( {summaryData && summaryData.unique_models.length > 0 && (
<Card> <Card>
<CardHeader> <CardHeader>
@@ -306,7 +328,9 @@ export default function DashboardPage() {
key={type} key={type}
className='flex items-center justify-between' className='flex items-center justify-between'
> >
<span className='text-sm font-medium'>{type}</span> <span className='text-sm font-medium'>
{type}
</span>
<span className='text-muted-foreground text-sm'> <span className='text-muted-foreground text-sm'>
{count} {count}
</span> </span>
@@ -335,16 +359,18 @@ export default function DashboardPage() {
</div> </div>
{revenueByModelLoading ? ( {revenueByModelLoading ? (
<div className='text-center py-8'>Loading revenue by model...</div> <div className='py-8 text-center'>
Loading revenue by model...
</div>
) : revenueByModelData && revenueByModelData.models.length > 0 ? ( ) : revenueByModelData && revenueByModelData.models.length > 0 ? (
<RevenueByModelTable <RevenueByModelTable
models={revenueByModelData.models} models={revenueByModelData.models}
totalRevenue={revenueByModelData.total_revenue_sats} totalRevenue={revenueByModelData.total_revenue_sats}
/> />
) : null} ) : null}
{errorLoading ? ( {errorLoading ? (
<div className='text-center py-8'>Loading errors...</div> <div className='py-8 text-center'>Loading errors...</div>
) : errorData ? ( ) : errorData ? (
<ErrorDetailsTable errors={errorData.errors} /> <ErrorDetailsTable errors={errorData.errors} />
) : null} ) : null}
+73 -34
View File
@@ -76,7 +76,11 @@ function ProviderBalance({
); );
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: balanceData, isLoading, error } = useQuery({ const {
data: balanceData,
isLoading,
error,
} = useQuery({
queryKey: ['provider-balance', providerId], queryKey: ['provider-balance', providerId],
queryFn: () => AdminService.getProviderBalance(providerId), queryFn: () => AdminService.getProviderBalance(providerId),
refetchInterval: 30000, refetchInterval: 30000,
@@ -108,7 +112,10 @@ function ProviderBalance({
mutationFn: async (amount: number) => { mutationFn: async (amount: number) => {
console.log('Calling top-up API with:', { providerId, amount }); console.log('Calling top-up API with:', { providerId, amount });
try { try {
const result = await AdminService.initiateProviderTopup(providerId, amount); const result = await AdminService.initiateProviderTopup(
providerId,
amount
);
console.log('API returned:', result); console.log('API returned:', result);
return result; return result;
} catch (err) { } catch (err) {
@@ -120,11 +127,8 @@ function ProviderBalance({
console.log('Top-up response:', data); console.log('Top-up response:', data);
console.log('Type of data:', typeof data); console.log('Type of data:', typeof data);
console.log('Keys in data:', Object.keys(data || {})); console.log('Keys in data:', Object.keys(data || {}));
if ( if (data?.topup_data?.payment_request && data?.topup_data?.invoice_id) {
data?.topup_data?.payment_request &&
data?.topup_data?.invoice_id
) {
setInvoiceData({ setInvoiceData({
payment_request: data.topup_data.payment_request as string, payment_request: data.topup_data.payment_request as string,
invoice_id: data.topup_data.invoice_id as string, invoice_id: data.topup_data.invoice_id as string,
@@ -172,19 +176,23 @@ function ProviderBalance({
// The backend throws 500/400 if not implemented. // The backend throws 500/400 if not implemented.
// A better approach is to check if we have a platform URL and maybe redirect there // A better approach is to check if we have a platform URL and maybe redirect there
// if we know it's not supported. // if we know it's not supported.
// BUT, we don't know for sure if it's supported without checking metadata or trying. // BUT, we don't know for sure if it's supported without checking metadata or trying.
// Let's rely on the "can_topup" metadata if available, but currently we only have "can_show_balance". // Let's rely on the "can_topup" metadata if available, but currently we only have "can_show_balance".
// Simple heuristic: If platformUrl exists and we suspect no direct topup, redirect? // Simple heuristic: If platformUrl exists and we suspect no direct topup, redirect?
// Actually, let's try to open the dialog, but if it's OpenRouter/OpenAI, maybe we just redirect? // Actually, let's try to open the dialog, but if it's OpenRouter/OpenAI, maybe we just redirect?
// The user specifically mentioned "like in openrouter". // The user specifically mentioned "like in openrouter".
if (platformUrl && (platformUrl.includes('openrouter.ai') || platformUrl.includes('openai.com'))) { if (
window.open(platformUrl, '_blank'); platformUrl &&
return; (platformUrl.includes('openrouter.ai') ||
platformUrl.includes('openai.com'))
) {
window.open(platformUrl, '_blank');
return;
} }
setIsTopupDialogOpen(true); setIsTopupDialogOpen(true);
}; };
@@ -266,9 +274,7 @@ function ProviderBalance({
<path d='M5 13l4 4L19 7'></path> <path d='M5 13l4 4L19 7'></path>
</svg> </svg>
</div> </div>
<p className='text-center font-semibold'> <p className='text-center font-semibold'>Top-up successful!</p>
Top-up successful!
</p>
</div> </div>
) : invoiceData ? ( ) : invoiceData ? (
<div className='flex flex-col items-center gap-4 py-4'> <div className='flex flex-col items-center gap-4 py-4'>
@@ -482,7 +488,9 @@ export default function ProvidersPage() {
...formData, ...formData,
api_key: String(response.account_data.api_key), api_key: String(response.account_data.api_key),
}); });
toast.success('Account created successfully! API key has been filled in.'); toast.success(
'Account created successfully! API key has been filled in.'
);
} else { } else {
toast.success('Account created, but no API key returned.'); toast.success('Account created, but no API key returned.');
} }
@@ -778,8 +786,7 @@ export default function ProvidersPage() {
)} )}
/> />
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
1.01 means +1% e.g. currency exchange, card 1.01 means +1% e.g. currency exchange, card fees, etc.
fees, etc.
</p> </p>
</div> </div>
</div> </div>
@@ -856,9 +863,11 @@ export default function ProvidersPage() {
<div className='flex flex-wrap items-center gap-2'> <div className='flex flex-wrap items-center gap-2'>
{canShowBalance(provider.provider_type) && {canShowBalance(provider.provider_type) &&
provider.api_key && ( provider.api_key && (
<ProviderBalance <ProviderBalance
providerId={provider.id} providerId={provider.id}
platformUrl={getPlatformUrl(provider.provider_type)} platformUrl={getPlatformUrl(
provider.provider_type
)}
/> />
)} )}
<Button <Button
@@ -925,9 +934,16 @@ export default function ProvidersPage() {
{providerModels.db_models.length === 0 ? ( {providerModels.db_models.length === 0 ? (
<div className='flex flex-col items-center justify-center gap-2 py-4'> <div className='flex flex-col items-center justify-center gap-2 py-4'>
<div className='text-muted-foreground text-sm'> <div className='text-muted-foreground text-sm'>
No models configured. Add custom models to use this provider. No models configured. Add custom models
to use this provider.
</div> </div>
<Button variant='outline' size='sm' onClick={() => handleAddModel(provider.id)}> <Button
variant='outline'
size='sm'
onClick={() =>
handleAddModel(provider.id)
}
>
<Plus className='mr-2 h-4 w-4' /> <Plus className='mr-2 h-4 w-4' />
Add Custom Model Add Custom Model
</Button> </Button>
@@ -970,7 +986,12 @@ export default function ProvidersPage() {
variant='ghost' variant='ghost'
size='icon' size='icon'
className='h-8 w-8' className='h-8 w-8'
onClick={() => handleEditModel(provider.id, model)} onClick={() =>
handleEditModel(
provider.id,
model
)
}
> >
<Pencil className='h-4 w-4' /> <Pencil className='h-4 w-4' />
</Button> </Button>
@@ -1027,10 +1048,17 @@ export default function ProvidersPage() {
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
{providerModels.db_models.length > 0 && ( {providerModels.db_models.length > 0 && (
<div className='text-muted-foreground text-sm'> <div className='text-muted-foreground text-sm'>
Custom models override or extend the provider&apos;s catalog. Custom models override or extend the
provider&apos;s catalog.
</div> </div>
)} )}
<Button variant='outline' size='sm' onClick={() => handleAddModel(provider.id)}> <Button
variant='outline'
size='sm'
onClick={() =>
handleAddModel(provider.id)
}
>
<Plus className='mr-2 h-4 w-4' /> <Plus className='mr-2 h-4 w-4' />
Add Add
</Button> </Button>
@@ -1079,7 +1107,12 @@ export default function ProvidersPage() {
variant='ghost' variant='ghost'
size='icon' size='icon'
className='h-8 w-8' className='h-8 w-8'
onClick={() => handleEditModel(provider.id, model)} onClick={() =>
handleEditModel(
provider.id,
model
)
}
> >
<Pencil className='h-4 w-4' /> <Pencil className='h-4 w-4' />
</Button> </Button>
@@ -1126,7 +1159,12 @@ export default function ProvidersPage() {
variant='outline' variant='outline'
size='sm' size='sm'
className='h-7 text-xs' className='h-7 text-xs'
onClick={() => handleOverrideModel(provider.id, model)} onClick={() =>
handleOverrideModel(
provider.id,
model
)
}
> >
<Plus className='mr-1 h-3 w-3' /> <Plus className='mr-1 h-3 w-3' />
Override Override
@@ -1245,7 +1283,7 @@ export default function ProvidersPage() {
</div> </div>
)} )}
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
<Switch <Switch
id='edit_enabled' id='edit_enabled'
checked={formData.enabled} checked={formData.enabled}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
@@ -1277,8 +1315,7 @@ export default function ProvidersPage() {
)} )}
/> />
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
1.01 means +1% e.g. currency exchange, card 1.01 means +1% e.g. currency exchange, card fees, etc.
fees, etc.
</p> </p>
</div> </div>
</div> </div>
@@ -1303,7 +1340,9 @@ export default function ProvidersPage() {
<AddProviderModelDialog <AddProviderModelDialog
providerId={modelDialogState.providerId} providerId={modelDialogState.providerId}
isOpen={modelDialogState.isOpen} isOpen={modelDialogState.isOpen}
onClose={() => setModelDialogState((prev) => ({ ...prev, isOpen: false }))} onClose={() =>
setModelDialogState((prev) => ({ ...prev, isOpen: false }))
}
onSuccess={() => { onSuccess={() => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['provider-models', modelDialogState.providerId], queryKey: ['provider-models', modelDialogState.providerId],
+76 -41
View File
@@ -102,7 +102,8 @@ export function AddProviderModelDialog({
}: AddProviderModelDialogProps) { }: AddProviderModelDialogProps) {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [isPresetOpen, setIsPresetOpen] = useState(false); const [isPresetOpen, setIsPresetOpen] = useState(false);
const [selectedPresetLabel, setSelectedPresetLabel] = useState('Select a preset'); const [selectedPresetLabel, setSelectedPresetLabel] =
useState('Select a preset');
const form = useForm<FormData>({ const form = useForm<FormData>({
resolver: zodResolver(FormSchema) as never, resolver: zodResolver(FormSchema) as never,
@@ -149,7 +150,10 @@ export function AddProviderModelDialog({
if (initialData) { if (initialData) {
const architecture = initialData.architecture as Record<string, unknown>; const architecture = initialData.architecture as Record<string, unknown>;
const pricing = initialData.pricing as Record<string, number>; const pricing = initialData.pricing as Record<string, number>;
const topProvider = initialData.top_provider as Record<string, unknown> | null; const topProvider = initialData.top_provider as Record<
string,
unknown
> | null;
form.reset({ form.reset({
id: initialData.id, id: initialData.id,
@@ -250,7 +254,9 @@ export function AddProviderModelDialog({
form.setValue('context_length', model.context_length); form.setValue('context_length', model.context_length);
form.setValue( form.setValue(
'modality', 'modality',
typeof architecture?.modality === 'string' ? architecture.modality : 'text' typeof architecture?.modality === 'string'
? architecture.modality
: 'text'
); );
form.setValue( form.setValue(
'input_modalities_raw', 'input_modalities_raw',
@@ -318,7 +324,10 @@ export function AddProviderModelDialog({
setIsSubmitting(true); setIsSubmitting(true);
try { try {
let perRequestLimits: Record<string, unknown> | null = null; let perRequestLimits: Record<string, unknown> | null = null;
if (data.per_request_limits_raw && data.per_request_limits_raw.trim().length) { if (
data.per_request_limits_raw &&
data.per_request_limits_raw.trim().length
) {
try { try {
perRequestLimits = JSON.parse(data.per_request_limits_raw); perRequestLimits = JSON.parse(data.per_request_limits_raw);
} catch { } catch {
@@ -336,7 +345,9 @@ export function AddProviderModelDialog({
context_length: data.context_length, context_length: data.context_length,
architecture: { architecture: {
modality: data.modality, modality: data.modality,
input_modalities: listFromString(data.input_modalities_raw || data.modality), input_modalities: listFromString(
data.input_modalities_raw || data.modality
),
output_modalities: listFromString( output_modalities: listFromString(
data.output_modalities_raw || data.modality data.output_modalities_raw || data.modality
), ),
@@ -366,10 +377,9 @@ export function AddProviderModelDialog({
is_moderated: data.top_provider_is_moderated, is_moderated: data.top_provider_is_moderated,
} }
: null, : null,
upstream_provider_id: upstream_provider_id: data.upstream_provider_id?.trim().length
data.upstream_provider_id?.trim().length ? data.upstream_provider_id.trim()
? data.upstream_provider_id.trim() : providerId,
: providerId,
canonical_slug: data.canonical_slug?.trim() || null, canonical_slug: data.canonical_slug?.trim() || null,
alias_ids: listFromString(data.alias_ids_raw || ''), alias_ids: listFromString(data.alias_ids_raw || ''),
enabled: data.enabled, enabled: data.enabled,
@@ -416,7 +426,7 @@ export function AddProviderModelDialog({
<DialogDescription>{description}</DialogDescription> <DialogDescription>{description}</DialogDescription>
</DialogHeader> </DialogHeader>
{!isEdit && !isOverride && ( {!isEdit && !isOverride && (
<div className='rounded-md border bg-muted/30 p-3'> <div className='bg-muted/30 rounded-md border p-3'>
<div className='mb-2 text-sm font-medium'>Presets</div> <div className='mb-2 text-sm font-medium'>Presets</div>
<div className='grid gap-2 sm:grid-cols-3 sm:items-start'> <div className='grid gap-2 sm:grid-cols-3 sm:items-start'>
<Popover open={isPresetOpen} onOpenChange={setIsPresetOpen}> <Popover open={isPresetOpen} onOpenChange={setIsPresetOpen}>
@@ -425,23 +435,25 @@ export function AddProviderModelDialog({
variant='outline' variant='outline'
role='combobox' role='combobox'
aria-expanded={isPresetOpen} aria-expanded={isPresetOpen}
className='w-full justify-between text-left text-sm overflow-hidden' className='w-full justify-between overflow-hidden text-left text-sm'
> >
<span className='truncate'> <span className='truncate'>
{isLoadingPresets ? 'Loading presets...' : selectedPresetLabel} {isLoadingPresets
? 'Loading presets...'
: selectedPresetLabel}
</span> </span>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className='w-80 max-w-sm p-0' className='w-80 max-w-sm p-0'
align='start' align='start'
sideOffset={4} sideOffset={4}
collisionPadding={12} collisionPadding={12}
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}
> >
<Command shouldFilter={true}> <Command shouldFilter={true}>
<CommandInput placeholder='Search presets...' /> <CommandInput placeholder='Search presets...' />
<CommandList <CommandList
className='max-h-64 overflow-y-auto overscroll-contain' className='max-h-64 overflow-y-auto overscroll-contain'
onWheel={(e) => e.stopPropagation()} onWheel={(e) => e.stopPropagation()}
> >
@@ -475,8 +487,9 @@ export function AddProviderModelDialog({
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
<div className='text-muted-foreground text-xs mt-1'> <div className='text-muted-foreground mt-1 text-xs'>
Prefill fields from a preset model definition, then adjust as needed. Prefill fields from a preset model definition, then adjust as
needed.
</div> </div>
</div> </div>
)} )}
@@ -496,7 +509,9 @@ export function AddProviderModelDialog({
disabled={isOverride || isEdit} disabled={isOverride || isEdit}
/> />
</FormControl> </FormControl>
<FormDescription>Unique identifier for the model</FormDescription> <FormDescription>
Unique identifier for the model
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -524,7 +539,11 @@ export function AddProviderModelDialog({
<FormItem> <FormItem>
<FormLabel>Description</FormLabel> <FormLabel>Description</FormLabel>
<FormControl> <FormControl>
<Textarea placeholder='Brief description...' {...field} rows={2} /> <Textarea
placeholder='Brief description...'
{...field}
rows={2}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -575,10 +594,7 @@ export function AddProviderModelDialog({
<FormItem> <FormItem>
<FormLabel>Input Modalities</FormLabel> <FormLabel>Input Modalities</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder='text, image, file' {...field} />
placeholder='text, image, file'
{...field}
/>
</FormControl> </FormControl>
<FormDescription>Comma-separated list</FormDescription> <FormDescription>Comma-separated list</FormDescription>
<FormMessage /> <FormMessage />
@@ -593,10 +609,7 @@ export function AddProviderModelDialog({
<FormItem> <FormItem>
<FormLabel>Output Modalities</FormLabel> <FormLabel>Output Modalities</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder='text, image' {...field} />
placeholder='text, image'
{...field}
/>
</FormControl> </FormControl>
<FormDescription>Comma-separated list</FormDescription> <FormDescription>Comma-separated list</FormDescription>
<FormMessage /> <FormMessage />
@@ -643,7 +656,10 @@ export function AddProviderModelDialog({
<FormItem> <FormItem>
<FormLabel>Canonical Slug</FormLabel> <FormLabel>Canonical Slug</FormLabel>
<FormControl> <FormControl>
<Input placeholder='google/gemini-3-pro-preview-20251117' {...field} /> <Input
placeholder='google/gemini-3-pro-preview-20251117'
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -697,15 +713,19 @@ export function AddProviderModelDialog({
rows={4} rows={4}
/> />
</FormControl> </FormControl>
<FormDescription>JSON object; leave empty for none</FormDescription> <FormDescription>
JSON object; leave empty for none
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div> </div>
<div className='space-y-4 rounded-md border bg-muted/20 p-4'> <div className='bg-muted/20 space-y-4 rounded-md border p-4'>
<h4 className='text-sm font-medium'>Pricing (USD per 1M tokens)</h4> <h4 className='text-sm font-medium'>
Pricing (USD per 1M tokens)
</h4>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'> <div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<FormField <FormField
control={form.control} control={form.control}
@@ -827,7 +847,7 @@ export function AddProviderModelDialog({
</div> </div>
</div> </div>
<div className='space-y-4 rounded-md border bg-muted/20 p-4'> <div className='bg-muted/20 space-y-4 rounded-md border p-4'>
<h4 className='text-sm font-medium'>Top Provider (optional)</h4> <h4 className='text-sm font-medium'>Top Provider (optional)</h4>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-3'> <div className='grid grid-cols-1 gap-4 sm:grid-cols-3'>
<FormField <FormField
@@ -870,11 +890,18 @@ export function AddProviderModelDialog({
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'> <div className='space-y-0.5'>
<FormLabel className='text-base'>Is Moderated</FormLabel> <FormLabel className='text-base'>
<FormDescription>Whether provider enforces moderation</FormDescription> Is Moderated
</FormLabel>
<FormDescription>
Whether provider enforces moderation
</FormDescription>
</div> </div>
<FormControl> <FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} /> <Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl> </FormControl>
</FormItem> </FormItem>
)} )}
@@ -892,7 +919,10 @@ export function AddProviderModelDialog({
<FormDescription>Enable this model for use</FormDescription> <FormDescription>Enable this model for use</FormDescription>
</div> </div>
<FormControl> <FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} /> <Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl> </FormControl>
</FormItem> </FormItem>
)} )}
@@ -908,8 +938,14 @@ export function AddProviderModelDialog({
Cancel Cancel
</Button> </Button>
<Button type='submit' disabled={isSubmitting}> <Button type='submit' disabled={isSubmitting}>
{isSubmitting && <Loader2 className='mr-2 h-4 w-4 animate-spin' />} {isSubmitting && (
{isEdit ? 'Save Changes' : isOverride ? 'Create Override' : 'Create Model'} <Loader2 className='mr-2 h-4 w-4 animate-spin' />
)}
{isEdit
? 'Save Changes'
: isOverride
? 'Create Override'
: 'Create Model'}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>
@@ -918,4 +954,3 @@ export function AddProviderModelDialog({
</Dialog> </Dialog>
); );
} }
+13 -10
View File
@@ -2,11 +2,12 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { type Model, type GroupSettings } from '@/lib/api/schemas/models';
import { import {
type Model, AdminService,
type GroupSettings, type AdminModelGroup,
} from '@/lib/api/schemas/models'; type AdminModel,
import { AdminService, type AdminModelGroup, type AdminModel } from '@/lib/api/services/admin'; } from '@/lib/api/services/admin';
type ModelGroup = AdminModelGroup; type ModelGroup = AdminModelGroup;
import { AddProviderModelDialog } from '@/components/AddProviderModelDialog'; import { AddProviderModelDialog } from '@/components/AddProviderModelDialog';
import { EditGroupForm } from '@/components/EditGroupForm'; import { EditGroupForm } from '@/components/EditGroupForm';
@@ -514,7 +515,7 @@ export function ModelSelector({
return; return;
} }
const providerId = parseInt(model.provider_id); const providerId = parseInt(model.provider_id);
// Construct AdminModel from Model // Construct AdminModel from Model
const adminModel: AdminModel = { const adminModel: AdminModel = {
id: model.id, id: model.id,
@@ -557,7 +558,7 @@ export function ModelSelector({
return; return;
} }
const providerId = parseInt(model.provider_id); const providerId = parseInt(model.provider_id);
// Construct AdminModel from Model // Construct AdminModel from Model
const adminModel: AdminModel = { const adminModel: AdminModel = {
id: model.id, id: model.id,
@@ -901,10 +902,10 @@ export function ModelSelector({
)} )}
{/* Model Management Actions */} {/* Model Management Actions */}
{groupData && ( {groupData && (
<Button <Button
onClick={() => handleAddModelClick(parseInt(groupData.id))} onClick={() => handleAddModelClick(parseInt(groupData.id))}
className='gap-2' className='gap-2'
variant="outline" variant='outline'
> >
<CheckSquare className='h-4 w-4' /> <CheckSquare className='h-4 w-4' />
Add Custom Model Add Custom Model
@@ -1306,7 +1307,9 @@ export function ModelSelector({
<AddProviderModelDialog <AddProviderModelDialog
providerId={modelDialogState.providerId} providerId={modelDialogState.providerId}
isOpen={modelDialogState.isOpen} isOpen={modelDialogState.isOpen}
onClose={() => setModelDialogState((prev) => ({ ...prev, isOpen: false }))} onClose={() =>
setModelDialogState((prev) => ({ ...prev, isOpen: false }))
}
onSuccess={handleModelUpdate} onSuccess={handleModelUpdate}
initialData={modelDialogState.initialData} initialData={modelDialogState.initialData}
mode={modelDialogState.mode} mode={modelDialogState.mode}
+12 -7
View File
@@ -48,20 +48,26 @@ export function CurrencyToggle() {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-9 w-9 px-0 gap-2 w-auto px-3 font-normal"> <Button
<Coins className="h-4 w-4" /> variant='ghost'
<span className="hidden sm:inline-block">{getLabel(displayUnit)}</span> size='sm'
<span className="sm:hidden uppercase">{displayUnit}</span> className='h-9 w-9 w-auto gap-2 px-0 px-3 font-normal'
>
<Coins className='h-4 w-4' />
<span className='hidden sm:inline-block'>
{getLabel(displayUnit)}
</span>
<span className='uppercase sm:hidden'>{displayUnit}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => setDisplayUnit('msat')}> <DropdownMenuItem onClick={() => setDisplayUnit('msat')}>
Millisatoshis (mSAT) Millisatoshis (mSAT)
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setDisplayUnit('sat')}> <DropdownMenuItem onClick={() => setDisplayUnit('sat')}>
Satoshis (sat) Satoshis (sat)
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => setDisplayUnit('usd')} onClick={() => setDisplayUnit('usd')}
disabled={!usdPerSat} disabled={!usdPerSat}
> >
@@ -71,4 +77,3 @@ export function CurrencyToggle() {
</DropdownMenu> </DropdownMenu>
); );
} }
@@ -96,4 +96,3 @@ export function DashboardBalanceSummary({
</div> </div>
); );
} }
+1 -1
View File
@@ -24,7 +24,7 @@ export function ErrorDetailsTable({ errors }: ErrorDetailsTableProps) {
<CardTitle>Recent Errors</CardTitle> <CardTitle>Recent Errors</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className='text-muted-foreground text-center py-8'> <p className='text-muted-foreground py-8 text-center'>
No errors found in the selected time period No errors found in the selected time period
</p> </p>
</CardContent> </CardContent>
+45 -41
View File
@@ -116,7 +116,9 @@ export function CheatSheet(): JSX.Element {
const [topupToken, setTopupToken] = useState(''); const [topupToken, setTopupToken] = useState('');
const [apiKeyInput, setApiKeyInput] = useState(''); const [apiKeyInput, setApiKeyInput] = useState('');
const [walletInfo, setWalletInfo] = useState<WalletSnapshot | null>(null); const [walletInfo, setWalletInfo] = useState<WalletSnapshot | null>(null);
const [refundReceipt, setRefundReceipt] = useState<RefundReceipt | null>(null); const [refundReceipt, setRefundReceipt] = useState<RefundReceipt | null>(
null
);
const [isCreatingKey, setIsCreatingKey] = useState(false); const [isCreatingKey, setIsCreatingKey] = useState(false);
const [isTopupLoading, setIsTopupLoading] = useState(false); const [isTopupLoading, setIsTopupLoading] = useState(false);
const [isRefunding, setIsRefunding] = useState(false); const [isRefunding, setIsRefunding] = useState(false);
@@ -332,22 +334,21 @@ export function CheatSheet(): JSX.Element {
const showCreateDetails = const showCreateDetails =
hasInteractedCreate || initialToken.trim().length > 0; hasInteractedCreate || initialToken.trim().length > 0;
const showManageDetails = hasInteractedManage || Boolean(walletInfo); const showManageDetails = hasInteractedManage || Boolean(walletInfo);
const showTopupDetails = const showTopupDetails = hasInteractedTopup || topupToken.trim().length > 0;
hasInteractedTopup || topupToken.trim().length > 0;
const refundToken = refundReceipt?.token ?? null; const refundToken = refundReceipt?.token ?? null;
const canTopup = Boolean(activeApiKey); const canTopup = Boolean(activeApiKey);
return ( return (
<div className='min-h-screen bg-gradient-to-b from-background via-background to-muted'> <div className='from-background via-background to-muted min-h-screen bg-gradient-to-b'>
<main className='mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8'> <main className='mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8'>
<section className='relative space-y-3 text-center md:text-left'> <section className='relative space-y-3 text-center md:text-left'>
<div className='absolute right-0 top-0 hidden md:block'> <div className='absolute top-0 right-0 hidden md:block'>
<Button asChild size='sm' className='px-3 text-xs'> <Button asChild size='sm' className='px-3 text-xs'>
<a href='/login'>Admin</a> <a href='/login'>Admin</a>
</Button> </Button>
</div> </div>
<div className='inline-flex items-center gap-2 rounded-full border px-3 py-1 text-[0.65rem] uppercase tracking-wider text-muted-foreground'> <div className='text-muted-foreground inline-flex items-center gap-2 rounded-full border px-3 py-1 text-[0.65rem] tracking-wider uppercase'>
<Bolt className='h-4 w-4 text-primary' /> <Bolt className='text-primary h-4 w-4' />
Routstr cheat sheet Routstr cheat sheet
</div> </div>
<h1 className='text-3xl font-semibold tracking-tight sm:text-4xl'> <h1 className='text-3xl font-semibold tracking-tight sm:text-4xl'>
@@ -365,10 +366,10 @@ export function CheatSheet(): JSX.Element {
<CardHeader className='flex flex-row items-start justify-between gap-4'> <CardHeader className='flex flex-row items-start justify-between gap-4'>
<div> <div>
<CardTitle className='flex items-center gap-2 text-lg'> <CardTitle className='flex items-center gap-2 text-lg'>
<ShieldCheck className='h-4 w-4 text-primary' /> <ShieldCheck className='text-primary h-4 w-4' />
Node identity Node identity
</CardTitle> </CardTitle>
<p className='text-xs uppercase tracking-wide text-muted-foreground'> <p className='text-muted-foreground text-xs tracking-wide uppercase'>
/v1/info snapshot /v1/info snapshot
</p> </p>
</div> </div>
@@ -404,7 +405,7 @@ export function CheatSheet(): JSX.Element {
</div> </div>
<dl className='grid gap-4 sm:grid-cols-2'> <dl className='grid gap-4 sm:grid-cols-2'>
<div> <div>
<dt className='text-muted-foreground text-xs uppercase tracking-wide'> <dt className='text-muted-foreground text-xs tracking-wide uppercase'>
Version Version
</dt> </dt>
<dd className='text-base font-medium'> <dd className='text-base font-medium'>
@@ -412,7 +413,7 @@ export function CheatSheet(): JSX.Element {
</dd> </dd>
</div> </div>
<div> <div>
<dt className='text-muted-foreground text-xs uppercase tracking-wide'> <dt className='text-muted-foreground text-xs tracking-wide uppercase'>
HTTP HTTP
</dt> </dt>
<dd className='text-base font-medium break-all'> <dd className='text-base font-medium break-all'>
@@ -421,7 +422,7 @@ export function CheatSheet(): JSX.Element {
</div> </div>
{nodeInfo.onion_url && ( {nodeInfo.onion_url && (
<div className='sm:col-span-2'> <div className='sm:col-span-2'>
<dt className='text-muted-foreground text-xs uppercase tracking-wide'> <dt className='text-muted-foreground text-xs tracking-wide uppercase'>
Onion Onion
</dt> </dt>
<dd className='text-base font-medium break-all'> <dd className='text-base font-medium break-all'>
@@ -431,10 +432,10 @@ export function CheatSheet(): JSX.Element {
)} )}
{nodeInfo.npub && ( {nodeInfo.npub && (
<div className='sm:col-span-2'> <div className='sm:col-span-2'>
<dt className='text-muted-foreground text-xs uppercase tracking-wide'> <dt className='text-muted-foreground text-xs tracking-wide uppercase'>
npub npub
</dt> </dt>
<dd className='flex items-center gap-2 break-all text-sm font-mono'> <dd className='flex items-center gap-2 font-mono text-sm break-all'>
{nodeInfo.npub} {nodeInfo.npub}
<Button <Button
variant='ghost' variant='ghost'
@@ -449,7 +450,7 @@ export function CheatSheet(): JSX.Element {
)} )}
</dl> </dl>
<div className='space-y-2'> <div className='space-y-2'>
<p className='text-xs uppercase tracking-wide text-muted-foreground'> <p className='text-muted-foreground text-xs tracking-wide uppercase'>
Cashu mints Cashu mints
</p> </p>
<div className='flex flex-wrap gap-2'> <div className='flex flex-wrap gap-2'>
@@ -478,10 +479,10 @@ export function CheatSheet(): JSX.Element {
<Card> <Card>
<CardHeader className='flex flex-row items-center justify-between'> <CardHeader className='flex flex-row items-center justify-between'>
<CardTitle className='flex items-center gap-2 text-lg'> <CardTitle className='flex items-center gap-2 text-lg'>
<Terminal className='h-4 w-4 text-primary' /> <Terminal className='text-primary h-4 w-4' />
Quick docs Quick docs
</CardTitle> </CardTitle>
<span className='text-xs uppercase tracking-wide text-muted-foreground'> <span className='text-muted-foreground text-xs tracking-wide uppercase'>
curl-ready curl-ready
</span> </span>
</CardHeader> </CardHeader>
@@ -502,8 +503,10 @@ export function CheatSheet(): JSX.Element {
<Copy className='h-4 w-4' /> <Copy className='h-4 w-4' />
</Button> </Button>
</div> </div>
<div className='rounded-lg bg-muted p-4 font-mono text-sm leading-6'> <div className='bg-muted rounded-lg p-4 font-mono text-sm leading-6'>
<pre className='whitespace-pre-wrap break-all'>{curlSnippet}</pre> <pre className='break-all whitespace-pre-wrap'>
{curlSnippet}
</pre>
</div> </div>
<div className='flex gap-2'> <div className='flex gap-2'>
<Button <Button
@@ -532,16 +535,16 @@ export function CheatSheet(): JSX.Element {
<Card> <Card>
<CardHeader className='space-y-1'> <CardHeader className='space-y-1'>
<CardTitle className='flex items-center gap-2 text-xl'> <CardTitle className='flex items-center gap-2 text-xl'>
<KeyRound className='h-5 w-5 text-primary' /> <KeyRound className='text-primary h-5 w-5' />
API key workflow API key workflow
</CardTitle> </CardTitle>
<p className='text-xs uppercase tracking-wide text-muted-foreground'> <p className='text-muted-foreground text-xs tracking-wide uppercase'>
Sections expand as soon as you interact Sections expand as soon as you interact
</p> </p>
</CardHeader> </CardHeader>
<CardContent className='space-y-6'> <CardContent className='space-y-6'>
<section className='space-y-2'> <section className='space-y-2'>
<header className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'> <header className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
<span>1 · Create key</span> <span>1 · Create key</span>
{showCreateDetails && ( {showCreateDetails && (
<span className='text-primary'>Cashu token detected</span> <span className='text-primary'>Cashu token detected</span>
@@ -564,7 +567,7 @@ export function CheatSheet(): JSX.Element {
> >
{isCreatingKey ? 'Creating…' : 'Create API key'} {isCreatingKey ? 'Creating…' : 'Create API key'}
</Button> </Button>
<span className='text-xs text-muted-foreground'> <span className='text-muted-foreground text-xs'>
Redeems instantly and returns <code>sk-</code> key. Redeems instantly and returns <code>sk-</code> key.
</span> </span>
</div> </div>
@@ -574,7 +577,7 @@ export function CheatSheet(): JSX.Element {
<Separator /> <Separator />
<section className='space-y-2'> <section className='space-y-2'>
<header className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'> <header className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
<span>2 · Manage key</span> <span>2 · Manage key</span>
{walletInfo && ( {walletInfo && (
<span className='text-primary'> <span className='text-primary'>
@@ -618,7 +621,7 @@ export function CheatSheet(): JSX.Element {
{showManageDetails && ( {showManageDetails && (
<div className='grid gap-3 sm:grid-cols-2'> <div className='grid gap-3 sm:grid-cols-2'>
<div className='rounded-lg border p-3'> <div className='rounded-lg border p-3'>
<p className='text-[0.65rem] uppercase tracking-wide text-muted-foreground'> <p className='text-muted-foreground text-[0.65rem] tracking-wide uppercase'>
Spendable Spendable
</p> </p>
<p className='text-xl font-semibold'> <p className='text-xl font-semibold'>
@@ -633,7 +636,7 @@ export function CheatSheet(): JSX.Element {
)} )}
</div> </div>
<div className='rounded-lg border p-3'> <div className='rounded-lg border p-3'>
<p className='text-[0.65rem] uppercase tracking-wide text-muted-foreground'> <p className='text-muted-foreground text-[0.65rem] tracking-wide uppercase'>
Reserved Reserved
</p> </p>
<p className='text-xl font-semibold'> <p className='text-xl font-semibold'>
@@ -654,10 +657,12 @@ export function CheatSheet(): JSX.Element {
<Separator /> <Separator />
<section className='space-y-2'> <section className='space-y-2'>
<header className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'> <header className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
<span>3 · Top up</span> <span>3 · Top up</span>
{showTopupDetails && ( {showTopupDetails && (
<span className={canTopup ? 'text-primary' : 'text-destructive'}> <span
className={canTopup ? 'text-primary' : 'text-destructive'}
>
{canTopup ? 'Ready to redeem' : 'Paste API key first'} {canTopup ? 'Ready to redeem' : 'Paste API key first'}
</span> </span>
)} )}
@@ -681,14 +686,14 @@ export function CheatSheet(): JSX.Element {
> >
{isTopupLoading ? 'Topping up…' : 'Top up this key'} {isTopupLoading ? 'Topping up…' : 'Top up this key'}
</Button> </Button>
<span className='text-xs text-muted-foreground'> <span className='text-muted-foreground text-xs'>
{canTopup {canTopup ? (
? ( <>
<> Adds balance to the same <code>sk-</code> token.
Adds balance to the same <code>sk-</code> token. </>
</> ) : (
) 'Enter your sk- key above to unlock top ups.'
: 'Enter your sk- key above to unlock top ups.'} )}
</span> </span>
</div> </div>
)} )}
@@ -697,7 +702,7 @@ export function CheatSheet(): JSX.Element {
<Separator /> <Separator />
<section className='space-y-2'> <section className='space-y-2'>
<header className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'> <header className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
<span>4 · Refund</span> <span>4 · Refund</span>
{refundReceipt && <span className='text-primary'>Done</span>} {refundReceipt && <span className='text-primary'>Done</span>}
</header> </header>
@@ -710,13 +715,13 @@ export function CheatSheet(): JSX.Element {
> >
{isRefunding ? 'Processing…' : 'Refund remaining balance'} {isRefunding ? 'Processing…' : 'Refund remaining balance'}
</Button> </Button>
<span className='text-xs text-muted-foreground'> <span className='text-muted-foreground text-xs'>
Burns the key and returns a fresh Cashu token. Burns the key and returns a fresh Cashu token.
</span> </span>
</div> </div>
{refundToken && ( {refundToken && (
<div className='space-y-2 rounded-lg border bg-muted/30 p-4'> <div className='bg-muted/30 space-y-2 rounded-lg border p-4'>
<div className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'> <div className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
<span>Cashu refund token</span> <span>Cashu refund token</span>
<Button <Button
variant='outline' variant='outline'
@@ -743,4 +748,3 @@ export function CheatSheet(): JSX.Element {
</div> </div>
); );
} }
+40 -23
View File
@@ -41,8 +41,11 @@ export function RevenueByModelTable({
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Revenue by Model</CardTitle> <CardTitle>Revenue by Model</CardTitle>
<p className="text-sm text-muted-foreground"> <p className='text-muted-foreground text-sm'>
Total Revenue: <span className="font-mono font-medium text-foreground">{formatAmount(totalRevenue)}</span> Total Revenue:{' '}
<span className='text-foreground font-mono font-medium'>
{formatAmount(totalRevenue)}
</span>
</p> </p>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -50,48 +53,62 @@ export function RevenueByModelTable({
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Model</TableHead> <TableHead>Model</TableHead>
<TableHead className="text-right">Requests</TableHead> <TableHead className='text-right'>Requests</TableHead>
<TableHead className="text-right">Successful</TableHead> <TableHead className='text-right'>Successful</TableHead>
<TableHead className="text-right">Failed</TableHead> <TableHead className='text-right'>Failed</TableHead>
<TableHead className="text-right">Revenue</TableHead> <TableHead className='text-right'>Revenue</TableHead>
<TableHead className="w-[100px]">Share</TableHead> <TableHead className='w-[100px]'>Share</TableHead>
<TableHead className="text-right">Refunds</TableHead> <TableHead className='text-right'>Refunds</TableHead>
<TableHead className="text-right">Net Revenue</TableHead> <TableHead className='text-right'>Net Revenue</TableHead>
<TableHead className="text-right">Avg/Request</TableHead> <TableHead className='text-right'>Avg/Request</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{models.length === 0 ? ( {models.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={9} className="text-center text-muted-foreground"> <TableCell
colSpan={9}
className='text-muted-foreground text-center'
>
No model data available No model data available
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
models.map((model) => { models.map((model) => {
const share = totalRevenue > 0 ? (model.revenue_sats / totalRevenue) * 100 : 0; const share =
totalRevenue > 0
? (model.revenue_sats / totalRevenue) * 100
: 0;
return ( return (
<TableRow key={model.model}> <TableRow key={model.model}>
<TableCell className="font-medium">{model.model}</TableCell> <TableCell className='font-medium'>{model.model}</TableCell>
<TableCell className="text-right font-mono">{model.requests}</TableCell> <TableCell className='text-right font-mono'>
<TableCell className="text-right text-green-600 font-mono">{model.successful}</TableCell> {model.requests}
<TableCell className="text-right text-red-600 font-mono">{model.failed}</TableCell> </TableCell>
<TableCell className="text-right font-mono"> <TableCell className='text-right font-mono text-green-600'>
{model.successful}
</TableCell>
<TableCell className='text-right font-mono text-red-600'>
{model.failed}
</TableCell>
<TableCell className='text-right font-mono'>
{formatAmount(model.revenue_sats)} {formatAmount(model.revenue_sats)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className='flex items-center gap-2'>
<Progress value={share} className="h-2" /> <Progress value={share} className='h-2' />
<span className="text-xs text-muted-foreground w-8 text-right">{share.toFixed(0)}%</span> <span className='text-muted-foreground w-8 text-right text-xs'>
{share.toFixed(0)}%
</span>
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right text-red-500 font-mono"> <TableCell className='text-right font-mono text-red-500'>
{formatAmount(model.refunds_sats)} {formatAmount(model.refunds_sats)}
</TableCell> </TableCell>
<TableCell className="text-right font-semibold font-mono"> <TableCell className='text-right font-mono font-semibold'>
{formatAmount(model.net_revenue_sats)} {formatAmount(model.net_revenue_sats)}
</TableCell> </TableCell>
<TableCell className="text-right text-muted-foreground font-mono"> <TableCell className='text-muted-foreground text-right font-mono'>
{formatAmount(model.avg_revenue_per_request)} {formatAmount(model.avg_revenue_per_request)}
</TableCell> </TableCell>
</TableRow> </TableRow>
+5 -1
View File
@@ -90,7 +90,11 @@ export function UsageMetricsChart({
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
}} }}
itemStyle={{ fontSize: '12px' }} itemStyle={{ fontSize: '12px' }}
labelStyle={{ fontSize: '12px', color: 'hsl(var(--muted-foreground))', marginBottom: '8px' }} labelStyle={{
fontSize: '12px',
color: 'hsl(var(--muted-foreground))',
marginBottom: '8px',
}}
/> />
<Legend wrapperStyle={{ fontSize: '12px', paddingTop: '16px' }} /> <Legend wrapperStyle={{ fontSize: '12px', paddingTop: '16px' }} />
{dataKeys.map((dataKey) => ( {dataKeys.map((dataKey) => (
+9 -5
View File
@@ -114,15 +114,19 @@ export function UsageSummaryCards({ summary }: UsageSummaryCardsProps) {
return ( return (
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-4'> <div className='grid gap-6 md:grid-cols-2 lg:grid-cols-4'>
{cards.map((card) => ( {cards.map((card) => (
<Card key={card.title} className="hover:bg-muted/50 transition-colors"> <Card key={card.title} className='hover:bg-muted/50 transition-colors'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'> <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium text-muted-foreground'>{card.title}</CardTitle> <CardTitle className='text-muted-foreground text-sm font-medium'>
<div className={`p-2 rounded-full bg-secondary`}> {card.title}
<card.icon className={`h-4 w-4 ${card.color}`} /> </CardTitle>
<div className={`bg-secondary rounded-full p-2`}>
<card.icon className={`h-4 w-4 ${card.color}`} />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='text-2xl font-bold tracking-tight'>{card.value}</div> <div className='text-2xl font-bold tracking-tight'>
{card.value}
</div>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
+13 -8
View File
@@ -816,7 +816,9 @@ export class AdminService {
if (search) params.append('search', search); if (search) params.append('search', search);
params.append('limit', limit.toString()); params.append('limit', limit.toString());
return await apiClient.get<LogResponse>(`/admin/api/logs?${params.toString()}`); return await apiClient.get<LogResponse>(
`/admin/api/logs?${params.toString()}`
);
} }
static async getLogDates(): Promise<{ dates: string[] }> { static async getLogDates(): Promise<{ dates: string[] }> {
@@ -862,9 +864,7 @@ export class AdminService {
); );
} }
static async createProviderAccountByType( static async createProviderAccountByType(providerType: string): Promise<{
providerType: string
): Promise<{
ok: boolean; ok: boolean;
account_data: Record<string, unknown>; account_data: Record<string, unknown>;
message: string; message: string;
@@ -881,7 +881,11 @@ export class AdminService {
static async initiateProviderTopup( static async initiateProviderTopup(
providerId: number, providerId: number,
amount: number amount: number
): Promise<{ ok: boolean; topup_data: Record<string, unknown>; message: string }> { ): Promise<{
ok: boolean;
topup_data: Record<string, unknown>;
message: string;
}> {
return await apiClient.post<{ return await apiClient.post<{
ok: boolean; ok: boolean;
topup_data: Record<string, unknown>; topup_data: Record<string, unknown>;
@@ -901,9 +905,10 @@ export class AdminService {
}>(`/admin/api/upstream-providers/${providerId}/topup/${invoiceId}/status`); }>(`/admin/api/upstream-providers/${providerId}/topup/${invoiceId}/status`);
} }
static async getProviderBalance( static async getProviderBalance(providerId: number): Promise<{
providerId: number ok: boolean;
): Promise<{ ok: boolean; balance_data: number | null | Record<string, unknown> }> { balance_data: number | null | Record<string, unknown>;
}> {
return await apiClient.get<{ return await apiClient.get<{
ok: boolean; ok: boolean;
balance_data: number | null | Record<string, unknown>; balance_data: number | null | Record<string, unknown>;
-1
View File
@@ -18,4 +18,3 @@ export const useCurrencyStore = create<CurrencyState>()(
} }
) )
); );