- Fix model_pricing table references (model_id -> model, display_name -> model)
- Fix price columns (output_price_per_1k -> output_price_per_million)
- Add price conversion (per_million / 1000 = per_1k) in all API responses
- Add whereNotNull('model') filters to exclude invalid entries
- Add getModelDisplayName() helper method to all controllers
- Fix AccountController to use gateway_users budget fields directly
- Remove Budget model dependencies from AccountController
- Add custom Scramble server URL configuration for API docs
- Create ScrambleServiceProvider to set correct /api prefix
- Add migration to rename user_id to gateway_user_id in llm_requests
- Add custom ApiGuard for gateway_users authentication
- Update all API controllers: AccountController, ModelController, PricingController, ProviderController
All API endpoints now working correctly:
- GET /api/account
- GET /api/models
- GET /api/pricing
- GET /api/providers/{provider}
359 lines
18 KiB
PHP
359 lines
18 KiB
PHP
<x-app-layout>
|
|
<x-slot name="header">
|
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
|
{{ $user->alias ?? 'User Details' }}
|
|
</h2>
|
|
</x-slot>
|
|
|
|
<div class="py-12">
|
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
<!-- Header -->
|
|
<div class="mb-6 flex justify-between items-center">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">{{ $user->alias ?? 'User Details' }}</h1>
|
|
<p class="mt-1 text-sm text-gray-600 font-mono">{{ $user->user_id }}</p>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<form action="{{ route('gateway-users.toggle-block', $user->user_id) }}" method="POST" class="inline">
|
|
@csrf
|
|
<button type="submit"
|
|
class="inline-flex items-center px-4 py-2 bg-{{ $user->blocked ? 'green' : 'red' }}-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-{{ $user->blocked ? 'green' : 'red' }}-700">
|
|
{{ $user->blocked ? 'Unblock User' : 'Block User' }}
|
|
</button>
|
|
</form>
|
|
<a href="{{ route('gateway-users.edit', $user->user_id) }}"
|
|
class="inline-flex items-center px-4 py-2 bg-indigo-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-indigo-700">
|
|
Edit User
|
|
</a>
|
|
<form action="{{ route('gateway-users.destroy', $user->user_id) }}"
|
|
method="POST"
|
|
class="inline"
|
|
onsubmit="return confirm('Are you sure you want to delete this user? This will also delete all associated API keys and usage logs. This action cannot be undone!');">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit"
|
|
class="inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-700">
|
|
Delete User
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success Message -->
|
|
@if(session('success'))
|
|
<div class="mb-6 bg-green-50 border-l-4 border-green-400 p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm text-green-700">{{ session('success') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Stats Overview -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 p-3 bg-green-100 rounded-lg">
|
|
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-gray-600">Total Spend</p>
|
|
<p class="text-2xl font-bold text-gray-900">${{ number_format($user->spend, 2) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 p-3 bg-blue-100 rounded-lg">
|
|
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-gray-600">Requests (30d)</p>
|
|
<p class="text-2xl font-bold text-gray-900">{{ number_format($stats->total_requests ?? 0) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 p-3 bg-purple-100 rounded-lg">
|
|
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-gray-600">Tokens (30d)</p>
|
|
<p class="text-2xl font-bold text-gray-900">{{ number_format($stats->total_tokens ?? 0) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 p-3 bg-yellow-100 rounded-lg">
|
|
<svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-gray-600">API Keys</p>
|
|
<p class="text-2xl font-bold text-gray-900">{{ $user->apiKeys->count() }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Details & API Keys -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
<!-- User Details -->
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">User Information</h2>
|
|
<dl class="space-y-3">
|
|
<div class="flex justify-between">
|
|
<dt class="text-sm font-medium text-gray-600">User ID</dt>
|
|
<dd class="text-sm text-gray-900 font-mono">{{ $user->user_id }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-sm font-medium text-gray-600">Alias</dt>
|
|
<dd class="text-sm text-gray-900">{{ $user->alias ?? 'N/A' }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-sm font-medium text-gray-600">Status</dt>
|
|
<dd>
|
|
@if($user->blocked)
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
|
Blocked
|
|
</span>
|
|
@else
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
|
Active
|
|
</span>
|
|
@endif
|
|
</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-sm font-medium text-gray-600">Budget</dt>
|
|
<dd class="text-sm text-gray-900">
|
|
@if($user->budget)
|
|
{{ $user->budget->budget_id }} (${{ number_format($user->budget->max_budget, 2) }})
|
|
@else
|
|
No budget assigned
|
|
@endif
|
|
</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-sm font-medium text-gray-600">Created</dt>
|
|
<dd class="text-sm text-gray-900">{{ $user->created_at->format('M d, Y') }}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<!-- API Keys -->
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-lg font-semibold text-gray-900">API Keys</h2>
|
|
{{-- TODO: Enable when API Keys Management is implemented --}}
|
|
{{-- <a href="{{ route('keys.create', ['user_id' => $user->user_id]) }}"
|
|
class="text-sm text-indigo-600 hover:text-indigo-900">
|
|
+ Create Key
|
|
</a> --}}
|
|
<span class="text-sm text-gray-400">Coming soon</span>
|
|
</div>
|
|
@if($user->apiKeys->count() > 0)
|
|
<div class="space-y-3">
|
|
@foreach($user->apiKeys as $apiKey)
|
|
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
|
<div>
|
|
<div class="text-sm font-medium text-gray-900">{{ $apiKey->key_name ?? 'Unnamed' }}</div>
|
|
<div class="text-xs text-gray-500 font-mono">{{ substr($apiKey->id, 0, 12) }}...</div>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
@if($apiKey->is_active)
|
|
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
|
|
Active
|
|
</span>
|
|
@else
|
|
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-800">
|
|
Inactive
|
|
</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<p class="text-sm text-gray-500 text-center py-4">No API keys created yet</p>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 30-Day Usage Chart -->
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">Usage Trend (Last 30 Days)</h2>
|
|
<canvas id="usageChart" style="max-height: 300px;"></canvas>
|
|
</div>
|
|
|
|
<!-- 30-Day Statistics -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<div class="text-sm text-gray-600 mb-1">Cost (30d)</div>
|
|
<div class="text-2xl font-bold text-green-600">${{ number_format($stats->total_cost ?? 0, 2) }}</div>
|
|
<div class="text-xs text-gray-500 mt-1">
|
|
Avg: ${{ number_format(($stats->total_cost ?? 0) / max($stats->total_requests ?? 1, 1), 4) }}/request
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<div class="text-sm text-gray-600 mb-1">Prompt Tokens</div>
|
|
<div class="text-2xl font-bold text-blue-600">{{ number_format($stats->total_prompt_tokens ?? 0) }}</div>
|
|
<div class="text-xs text-gray-500 mt-1">
|
|
{{ number_format(($stats->total_prompt_tokens ?? 0) / max($stats->total_requests ?? 1, 1)) }}/request
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<div class="text-sm text-gray-600 mb-1">Completion Tokens</div>
|
|
<div class="text-2xl font-bold text-purple-600">{{ number_format($stats->total_completion_tokens ?? 0) }}</div>
|
|
<div class="text-xs text-gray-500 mt-1">
|
|
{{ number_format(($stats->total_completion_tokens ?? 0) / max($stats->total_requests ?? 1, 1)) }}/request
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Activity -->
|
|
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h2 class="text-lg font-semibold text-gray-900">Recent Activity</h2>
|
|
</div>
|
|
@if($recentLogs->count() > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Timestamp</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Model</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Provider</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tokens</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cost</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
@foreach($recentLogs->take(20) as $log)
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{{ $log->timestamp->format('M d, H:i') }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{{ $log->model }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
|
{{ $log->provider }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{{ number_format($log->total_tokens) }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
|
${{ number_format($log->cost, 4) }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
@if($log->status === 'success')
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
|
Success
|
|
</span>
|
|
@else
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
|
{{ $log->status }}
|
|
</span>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<div class="px-6 py-12 text-center">
|
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<h3 class="mt-2 text-sm font-medium text-gray-900">No activity yet</h3>
|
|
<p class="mt-1 text-sm text-gray-500">This user hasn't made any API requests yet.</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@push('scripts')
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
// Usage Chart
|
|
const usageCtx = document.getElementById('usageChart').getContext('2d');
|
|
new Chart(usageCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: @json($dailyUsage->pluck('date')->map(fn($d) => \Carbon\Carbon::parse($d)->format('M d'))),
|
|
datasets: [
|
|
{
|
|
label: 'Requests',
|
|
data: @json($dailyUsage->pluck('requests')),
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
yAxisID: 'y',
|
|
},
|
|
{
|
|
label: 'Cost ($)',
|
|
data: @json($dailyUsage->pluck('cost')),
|
|
borderColor: 'rgb(16, 185, 129)',
|
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
yAxisID: 'y1',
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
},
|
|
scales: {
|
|
y: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'left',
|
|
title: {
|
|
display: true,
|
|
text: 'Requests'
|
|
}
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'right',
|
|
title: {
|
|
display: true,
|
|
text: 'Cost ($)'
|
|
},
|
|
grid: {
|
|
drawOnChartArea: false,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
@endpush
|
|
|
|
</x-app-layout>
|