Refactor: Align system with gateway_users architecture

- Fixed database relationships: LlmRequest now properly uses gateway_user_id instead of user_id
- Updated Models: GatewayUser and LlmRequest relationships corrected
- Removed User->llmRequests relationship (admin users don't have LLM requests)
- Simplified Dashboard: Now shows Gateway User statistics instead of admin users
- Removed obsolete Budgets management pages (budgets handled directly in gateway_users)
- Removed User Budgets admin section (redundant with gateway_users management)
- Fixed view errors: Added null-checks for user_id in keys views
- Updated navigation: Removed Budget and User Budget links
- Updated routes: Cleaned up unused BudgetController and UserManagementController routes
- Simplified StatisticsService: Focus on gateway_users and basic metrics only
This commit is contained in:
Wilfried Trinkl
2025-11-19 21:13:59 +01:00
parent cb495e18e3
commit 602fe582b0
16 changed files with 91 additions and 751 deletions

View File

@@ -10,17 +10,17 @@
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Total Users -->
<!-- Total Gateway Users -->
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Total Users</p>
<p class="text-sm text-gray-600">Gateway Users</p>
<p class="text-3xl font-bold text-gray-900">
{{ number_format($stats['total_users']) }}
{{ number_format($stats['total_gateway_users']) }}
</p>
<p class="text-xs text-gray-500 mt-1">
{{ $stats['active_credentials'] }} active credentials
<p class="text-xs text-green-600 mt-1">
{{ $stats['active_gateway_users'] }} active
</p>
</div>
<div class="text-blue-500">
@@ -99,112 +99,58 @@
</div>
</div>
<!-- Usage Trend Chart -->
<!-- Daily Usage Chart -->
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Usage Trend (Last 30 Days)
</h3>
<canvas id="usageChart" height="80"></canvas>
<h3 class="text-lg font-semibold text-gray-900 mb-4">Daily Usage (Last 30 Days)</h3>
<canvas id="dailyUsageChart" height="80"></canvas>
</div>
</div>
<!-- Provider Stats & Top Users -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Provider Breakdown -->
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Usage by Provider
</h3>
<canvas id="providerChart" height="250"></canvas>
</div>
</div>
<!-- Top Users -->
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Top Users by Spend
</h3>
<div class="space-y-4">
@forelse($topUsers as $user)
<div class="flex items-center justify-between border-b border-gray-200 pb-3">
<div class="flex-1">
<p class="font-medium text-gray-900">
{{ $user->name }}
</p>
<p class="text-sm text-gray-500">
{{ number_format($user->llm_requests_count ?? 0) }} requests
</p>
</div>
<div class="text-right">
<p class="font-semibold text-green-600">
${{ number_format($user->total_cost ?? 0, 2) }}
</p>
<p class="text-xs text-gray-500">
{{ number_format($user->total_tokens ?? 0) }} tokens
</p>
</div>
</div>
@empty
<p class="text-gray-500 text-center py-4">
No usage data yet
</p>
@endforelse
</div>
</div>
</div>
</div>
<!-- Model Stats -->
<!-- Provider Statistics -->
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Most Used Models
</h3>
<h3 class="text-lg font-semibold text-gray-900 mb-4">Provider Statistics (Last 30 Days)</h3>
<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 tracking-wider">Model</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Provider</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">Tokens</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cost</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Provider
</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">
Total Cost
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Total Tokens
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($modelStats as $model)
@forelse($providerStats as $provider)
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{{ $model->model }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
@if($model->provider == 'openai') bg-green-100 text-green-800
@elseif($model->provider == 'anthropic') bg-purple-100 text-purple-800
@elseif($model->provider == 'mistral') bg-blue-100 text-blue-800
@elseif($model->provider == 'gemini') bg-yellow-100 text-yellow-800
@else bg-gray-100 text-gray-800
@endif">
{{ ucfirst($model->provider) }}
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
{{ $provider->provider }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ number_format($model->count) }}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ number_format($provider->count) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ number_format($model->tokens ?? 0) }}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${{ number_format($provider->total_cost, 2) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${{ number_format($model->total_cost ?? 0, 4) }}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ number_format($provider->total_tokens) }}
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-4 text-center text-gray-500">
No usage data yet
<td colspan="4" class="px-6 py-4 text-center text-sm text-gray-500">
No data available yet
</td>
</tr>
@endforelse
@@ -218,36 +164,34 @@
</div>
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
// Usage Trend Chart
const usageCtx = document.getElementById('usageChart').getContext('2d');
new Chart(usageCtx, {
// Daily Usage Chart
const dailyUsageCtx = document.getElementById('dailyUsageChart').getContext('2d');
const dailyUsageData = @json($dailyUsage);
new Chart(dailyUsageCtx, {
type: 'line',
data: {
labels: @json($dailyUsage->pluck('date')),
datasets: [
{
label: 'Requests',
data: @json($dailyUsage->pluck('requests')),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
yAxisID: 'y',
},
{
label: 'Cost ($)',
data: @json($dailyUsage->pluck('cost')),
borderColor: 'rgb(16, 185, 129)',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
tension: 0.4,
yAxisID: 'y1',
}
]
labels: dailyUsageData.map(d => d.date),
datasets: [{
label: 'Requests',
data: dailyUsageData.map(d => d.requests),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.1,
yAxisID: 'y',
}, {
label: 'Cost ($)',
data: dailyUsageData.map(d => d.cost),
borderColor: 'rgb(34, 197, 94)',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
tension: 0.1,
yAxisID: 'y1',
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
interaction: {
mode: 'index',
intersect: false,
@@ -277,51 +221,6 @@
}
}
});
// Provider Breakdown Chart
const providerCtx = document.getElementById('providerChart').getContext('2d');
new Chart(providerCtx, {
type: 'doughnut',
data: {
labels: @json($providerStats->pluck('provider')->map(fn($p) => ucfirst($p))),
datasets: [{
data: @json($providerStats->pluck('count')),
backgroundColor: [
'rgba(34, 197, 94, 0.8)', // Green - OpenAI
'rgba(168, 85, 247, 0.8)', // Purple - Anthropic
'rgba(59, 130, 246, 0.8)', // Blue - Mistral
'rgba(251, 191, 36, 0.8)', // Yellow - Gemini
'rgba(236, 72, 153, 0.8)', // Pink - DeepSeek
'rgba(249, 115, 22, 0.8)', // Orange
],
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: 'bottom',
labels: {
padding: 15
}
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.label || '';
let value = context.parsed || 0;
let total = context.dataset.data.reduce((a, b) => a + b, 0);
let percentage = ((value / total) * 100).toFixed(1);
return label + ': ' + value + ' requests (' + percentage + '%)';
}
}
}
}
}
});
</script>
@endpush
</x-app-layout>