Initial commit: Any-LLM Gateway with Laravel Admin Interface

- Any-LLM Gateway setup with Docker Compose
- Laravel 11 admin interface with Livewire
- Dashboard with usage statistics and charts
- Gateway Users management with budget tracking
- API Keys management with revocation
- Budget templates with assignment
- Usage Logs with filtering and CSV export
- Model Pricing management with calculator
- PostgreSQL database integration
- Complete authentication system for admins
This commit is contained in:
wtrinkl
2025-11-16 12:38:05 +01:00
commit b1363aeab9
148 changed files with 23995 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Create Gateway User') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900">Create Gateway User</h1>
<p class="mt-1 text-sm text-gray-600">Add a new API consumer to the gateway</p>
</div>
<!-- Form -->
<div class="bg-white rounded-lg shadow-sm p-6">
<form method="POST" action="{{ route('gateway-users.store') }}">
@csrf
<!-- Alias -->
<div class="mb-6">
<label for="alias" class="block text-sm font-medium text-gray-700 mb-2">
Alias (Optional)
</label>
<input type="text"
name="alias"
id="alias"
value="{{ old('alias') }}"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('alias') border-red-300 @enderror"
placeholder="My Application">
<p class="mt-1 text-sm text-gray-500">A friendly name to identify this user</p>
@error('alias')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Budget -->
<div class="mb-6">
<label for="budget_id" class="block text-sm font-medium text-gray-700 mb-2">
Budget Template (Optional)
</label>
<select name="budget_id"
id="budget_id"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('budget_id') border-red-300 @enderror">
<option value="">No Budget</option>
@foreach($budgets as $budget)
<option value="{{ $budget->budget_id }}" {{ old('budget_id') == $budget->budget_id ? 'selected' : '' }}>
{{ $budget->budget_id }} - ${{ number_format($budget->max_budget, 2) }}
({{ floor($budget->budget_duration_sec / 86400) }}d)
</option>
@endforeach
</select>
<p class="mt-1 text-sm text-gray-500">Assign a spending limit to this user</p>
@error('budget_id')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Info Box -->
<div class="mb-6 bg-blue-50 border-l-4 border-blue-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700">
A unique user ID will be automatically generated. You can create API keys for this user after creation.
</p>
</div>
</div>
</div>
<!-- Actions -->
<div class="flex items-center justify-end space-x-3">
<a href="{{ route('gateway-users.index') }}"
class="inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50">
Cancel
</a>
<button type="submit"
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">
Create User
</button>
</div>
</form>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,111 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Edit Gateway User') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900">Edit Gateway User</h1>
<p class="mt-1 text-sm text-gray-600">Update user settings and configuration</p>
</div>
<!-- Form -->
<div class="bg-white rounded-lg shadow-sm p-6">
<form method="POST" action="{{ route('gateway-users.update', $user->user_id) }}">
@csrf
@method('PUT')
<!-- User ID (Read-only) -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">
User ID
</label>
<input type="text"
value="{{ $user->user_id }}"
readonly
class="block w-full rounded-md border-gray-300 bg-gray-50 shadow-sm sm:text-sm font-mono">
<p class="mt-1 text-sm text-gray-500">Cannot be changed</p>
</div>
<!-- Alias -->
<div class="mb-6">
<label for="alias" class="block text-sm font-medium text-gray-700 mb-2">
Alias (Optional)
</label>
<input type="text"
name="alias"
id="alias"
value="{{ old('alias', $user->alias) }}"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('alias') border-red-300 @enderror"
placeholder="My Application">
@error('alias')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Budget -->
<div class="mb-6">
<label for="budget_id" class="block text-sm font-medium text-gray-700 mb-2">
Budget Template
</label>
<select name="budget_id"
id="budget_id"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('budget_id') border-red-300 @enderror">
<option value="">No Budget</option>
@foreach($budgets as $budget)
<option value="{{ $budget->budget_id }}"
{{ old('budget_id', $user->budget_id) == $budget->budget_id ? 'selected' : '' }}>
{{ $budget->budget_id }} - ${{ number_format($budget->max_budget, 2) }}
({{ floor($budget->budget_duration_sec / 86400) }}d)
</option>
@endforeach
</select>
@error('budget_id')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Current Spend (Read-only) -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">
Current Spend
</label>
<input type="text"
value="${{ number_format($user->spend, 2) }}"
readonly
class="block w-full rounded-md border-gray-300 bg-gray-50 shadow-sm sm:text-sm">
</div>
<!-- Actions -->
<div class="flex items-center justify-between">
<form action="{{ route('gateway-users.destroy', $user->user_id) }}"
method="POST"
onsubmit="return confirm('Are you sure? This will delete the user and all associated data.');">
@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 class="flex space-x-3">
<a href="{{ route('gateway-users.show', $user->user_id) }}"
class="inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50">
Cancel
</a>
<button type="submit"
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">
Update User
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,314 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Gateway Users') }}
</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">Gateway Users</h1>
<p class="mt-1 text-sm text-gray-600">Manage API consumers and their access</p>
</div>
<a href="{{ route('gateway-users.create') }}"
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">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Create User
</a>
</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
<!-- Filters -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<form method="GET" action="{{ route('gateway-users.index') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4" id="filterForm">
<div>
<label for="search" class="block text-sm font-medium text-gray-700">Search</label>
<input type="text"
name="search"
id="search"
value="{{ request('search') }}"
placeholder="User ID or Alias..."
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700">Status</label>
<select name="status"
id="status"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="">All Users</option>
<option value="active" {{ request('status') == 'active' ? 'selected' : '' }}>Active</option>
<option value="blocked" {{ request('status') == 'blocked' ? 'selected' : '' }}>Blocked</option>
</select>
</div>
<div>
<label for="sort" class="block text-sm font-medium text-gray-700">Sort By</label>
<select name="sort"
id="sort"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="created_at" {{ request('sort') == 'created_at' ? 'selected' : '' }}>Created Date</option>
<option value="spend" {{ request('sort') == 'spend' ? 'selected' : '' }}>Spend</option>
<option value="alias" {{ request('sort') == 'alias' ? 'selected' : '' }}>Alias</option>
</select>
</div>
<div class="flex items-end">
<button type="submit"
class="w-full inline-flex justify-center items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700">
Apply Filters
</button>
</div>
</form>
</div>
<!-- Bulk Actions Bar (hidden by default) -->
<div id="bulkActionsBar" class="bg-indigo-50 border border-indigo-200 rounded-lg p-4 mb-6 hidden">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="text-sm font-medium text-gray-700 mr-4">
<span id="selectedCount">0</span> user(s) selected
</span>
</div>
<div class="flex items-center space-x-3">
<form id="bulkActionForm" method="POST" action="{{ route('gateway-users.bulk-action') }}" class="flex items-center space-x-3">
@csrf
<input type="hidden" name="action" id="bulkActionType">
<div id="selectedUserIds"></div>
<button type="button"
onclick="executeBulkAction('block')"
class="inline-flex items-center px-3 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-700">
Block Selected
</button>
<button type="button"
onclick="executeBulkAction('unblock')"
class="inline-flex items-center px-3 py-2 bg-green-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-green-700">
Unblock Selected
</button>
<button type="button"
onclick="executeBulkAction('delete')"
class="inline-flex items-center px-3 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700">
Delete Selected
</button>
</form>
</div>
</div>
</div>
<!-- Users Table -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left">
<input type="checkbox"
id="selectAll"
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
onchange="toggleAllUsers(this)">
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Budget</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Spend</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">API Keys</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Requests</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($users as $user)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<input type="checkbox"
class="user-checkbox rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
value="{{ $user->user_id }}"
onchange="updateBulkActions()">
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-indigo-100 rounded-full">
<span class="text-indigo-600 font-semibold text-sm">
{{ strtoupper(substr($user->alias ?? $user->user_id, 0, 2)) }}
</span>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">
{{ $user->alias ?? 'N/A' }}
</div>
<div class="text-sm text-gray-500 font-mono">
{{ substr($user->user_id, 0, 20) }}...
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if($user->budget)
<div class="text-sm text-gray-900">{{ $user->budget->budget_id }}</div>
<div class="text-sm text-gray-500">${{ number_format($user->budget->max_budget, 2) }}</div>
@else
<span class="text-sm text-gray-400">No budget</span>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-semibold text-gray-900">${{ number_format($user->spend, 2) }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ $user->api_keys_count }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ number_format($user->usage_logs_count) }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@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
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('gateway-users.show', $user->user_id) }}"
class="text-indigo-600 hover:text-indigo-900 mr-3">View</a>
<a href="{{ route('gateway-users.edit', $user->user_id) }}"
class="text-yellow-600 hover:text-yellow-900 mr-3">Edit</a>
<form action="{{ route('gateway-users.toggle-block', $user->user_id) }}"
method="POST"
class="inline-block">
@csrf
<button type="submit"
class="text-{{ $user->blocked ? 'green' : 'red' }}-600 hover:text-{{ $user->blocked ? 'green' : 'red' }}-900">
{{ $user->blocked ? 'Unblock' : 'Block' }}
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<svg class="w-16 h-16 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-1">No users found</h3>
<p class="text-sm text-gray-500 mb-4">Get started by creating your first gateway user.</p>
<a href="{{ route('gateway-users.create') }}"
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">
Create User
</a>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
@if($users->hasPages())
<div class="mt-6">
{{ $users->links() }}
</div>
@endif
</div>
</div>
<script>
// Bulk Actions JavaScript
function toggleAllUsers(checkbox) {
const userCheckboxes = document.querySelectorAll('.user-checkbox');
userCheckboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateBulkActions();
}
function updateBulkActions() {
const checkedBoxes = document.querySelectorAll('.user-checkbox:checked');
const bulkActionsBar = document.getElementById('bulkActionsBar');
const selectedCount = document.getElementById('selectedCount');
const selectAll = document.getElementById('selectAll');
// Update count
selectedCount.textContent = checkedBoxes.length;
// Show/hide bulk actions bar
if (checkedBoxes.length > 0) {
bulkActionsBar.classList.remove('hidden');
} else {
bulkActionsBar.classList.add('hidden');
}
// Update "Select All" checkbox state
const allCheckboxes = document.querySelectorAll('.user-checkbox');
if (checkedBoxes.length === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
} else if (checkedBoxes.length === allCheckboxes.length) {
selectAll.checked = true;
selectAll.indeterminate = false;
} else {
selectAll.checked = false;
selectAll.indeterminate = true;
}
}
function executeBulkAction(action) {
const checkedBoxes = document.querySelectorAll('.user-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('Please select at least one user.');
return;
}
// Confirmation for delete action
if (action === 'delete') {
if (!confirm(`Are you sure you want to delete ${checkedBoxes.length} user(s)? This will also delete all associated API keys and usage logs. This action cannot be undone!`)) {
return;
}
}
// Set action type
document.getElementById('bulkActionType').value = action;
// Add selected user IDs as hidden inputs
const selectedUserIds = document.getElementById('selectedUserIds');
selectedUserIds.innerHTML = '';
checkedBoxes.forEach(checkbox => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'user_ids[]';
input.value = checkbox.value;
selectedUserIds.appendChild(input);
});
// Submit form
document.getElementById('bulkActionForm').submit();
}
</script>
</x-app-layout>

View File

@@ -0,0 +1,358 @@
<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('api-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>