@extends('layouts.app') @section('title', __('Operational metrics')) @section('page-title', __('Operational metrics')) @section('page-subtitle', __('Live view of the analysis pipeline + queue config.')) @push('head') @endpush @php $errorTone = $errorRatePct === null ? ['text' => 'text-slate-500 dark:text-slate-400', 'bar' => 'bg-slate-200 dark:bg-slate-700'] : ($errorRatePct >= 10 ? ['text' => 'text-rose-600 dark:text-rose-400', 'bar' => 'bg-rose-500'] : ($errorRatePct >= 3 ? ['text' => 'text-amber-600 dark:text-amber-400', 'bar' => 'bg-amber-500'] : ['text' => 'text-emerald-600 dark:text-emerald-400', 'bar' => 'bg-emerald-500'])); $errorBudget = $errorRatePct === null ? 0 : min(100, max(0, $errorRatePct)); $oldestHuman = function ($s) { if ($s === null) return '—'; if ($s < 60) return $s.'s'; if ($s < 3600) return floor($s / 60).'m '.($s % 60).'s'; return floor($s / 3600).'h '.floor(($s % 3600) / 60).'m'; }; // 8 unified stat tiles. Single visual system: heading + number + caption. $tiles = [ ['label' => __('Pending'), 'value' => number_format($pending), 'caption' => __('Awaiting a worker'), 'tone' => $pending > 50 ? 'amber' : 'neutral'], ['label' => __('Running'), 'value' => number_format($running), 'caption' => __('Currently with the LLM'), 'tone' => 'brand'], ['label' => __('Awaiting review'), 'value' => number_format($awaiting), 'caption' => __('Tenant admin queue'), 'tone' => 'neutral'], ['label' => __('Failed (24h)'), 'value' => number_format($failedRuns24h), 'caption' => __('Out of :n total runs', ['n' => $totalRuns24h]), 'tone' => $failedRuns24h > 0 ? 'rose' : 'neutral'], ['label' => __('Error rate (24h)'),'value' => $errorRatePct === null ? '—' : $errorRatePct.'%', 'caption' => null, 'tone' => 'error'], ['label' => __('Tokens (24h)'), 'value' => number_format($tokens24h), 'caption' => __('7d:').' '.number_format($tokens7d), 'tone' => 'neutral'], ['label' => __('p95 runtime'), 'value' => $runtimeP95 !== null ? $runtimeP95.'s' : '—', 'caption' => $runsCount.' '.__('runs'), 'tone' => $runtimeP95 !== null && $runtimeP95 > 180 ? 'amber' : 'neutral'], ['label' => __('Active workers'), 'value' => $activeWorkers === null ? '—' : (string) $activeWorkers, 'caption' => $oldestHuman($oldestPendingSeconds), 'tone' => $activeWorkers === 0 && ($pending > 0 || $running > 0) ? 'rose' : 'neutral'], ]; $toneClass = function (string $tone): string { return match ($tone) { 'brand' => 'text-brand-600 dark:text-brand-400', 'amber' => 'text-amber-600 dark:text-amber-400', 'rose' => 'text-rose-600 dark:text-rose-400', 'emerald' => 'text-emerald-600 dark:text-emerald-400', default => 'text-slate-900 dark:text-slate-100', }; }; @endphp @section('content')
{{-- ── HEADLINE TILES (8) ─────────────────────────────────────── --}}
@foreach($tiles as $t)
{{ $t['label'] }}
{{ $t['value'] }}
@if($t['tone'] === 'error')
@elseif($t['caption'])
{{ $t['caption'] }}
@endif
@endforeach
{{-- ── GLOBAL SETTINGS (compact form) ─────────────────────────── --}}
@csrf

{{ __('Global queue settings') }}

{{ __('Stored in platform_settings — applied on the next request, no redeploy needed.') }}

{{ __('Maximum outbound LLM requests per minute across all workers. Set this to whatever your provider tier allows.') }}

@error('llm_rpm')

{{ $message }}

@enderror

{{ __('Tenants without an explicit override get this many simultaneous analyses (pending + running). Anything beyond gets a friendly "please wait" message.') }}

@error('analysis_max_in_flight_per_tenant')

{{ $message }}

@enderror
{{-- ── WORKERS (read-only) ─────────────────────────────────────── --}}

{{ __('Workers') }}

{{ __('Workers are OS processes — they cannot be started or stopped from the web UI for security reasons. Run the command below under Supervisor (Linux) or NSSM/Task Scheduler (Windows).') }}

{{ __('Active') }}: {{ $activeWorkers === null ? '—' : $activeWorkers }}
{{ __('Recommended start command') }}
{{ $recommendedCommand }}

{{ __('The "analyses-priority" queue is consumed first, then "analyses", then "default". Start 8 workers per host to begin with — bump up only if you raise the LLM RPM cap.') }}

{{-- ── PER-TENANT QUEUE CONFIG ────────────────────────────────── --}}

{{ __('Per-tenant queue config') }}

{{ __('Toggle the priority lane and override the in-flight cap per tenant. Sorted by 24-hour activity.') }}

@if($tenants->isEmpty())
{{ __('No tenants yet.') }}
@else
@foreach($tenants as $t) @php $effectiveCap = $t->max_concurrent ?? $globalInFlightCap; $atCap = $t->in_flight >= $effectiveCap; @endphp @csrf @endforeach
{{ __('Tenant') }} {{ __('Priority') }} {{ __('Cap (override)') }} {{ __('In-flight') }} {{ __('24h') }}
{{ $t->name }}
#{{ $t->id }} · {{ $t->plan ?? '—' }}
priority_queue)> {{ $t->in_flight }} {{ $t->volume_24h }}
@if($tenants->hasPages())
{{ $tenants->onEachSide(1)->links() }}
@endif @endif
{{-- ── DIAGNOSTICS — QUEUE / STUCK / TOP TENANTS ─────────────── --}}
{{-- Queue depth --}}

{{ __('Queue depth') }}

{{ __('Connection') }}: {{ $queueConnection }}

@if($queueConnection !== 'database')
{{ __('Live queue depth is only computed when QUEUE_CONNECTION=database. Switch to database (or wire up a Redis dashboard) to see it here.') }}
@elseif(empty($queueDepths))
{{ __('All queues are empty.') }}
@else
    @foreach($queueDepths as $queue => $n)
  • {{ $queue }} {{ number_format($n) }}
  • @endforeach
@endif
{{-- Stuck or — when none — runtime quick stats --}} @if($stuck->count())

{{ __('Stuck analyses (>10 min in RUNNING)') }}

    @foreach($stuck as $a)
  • #{{ $a->id }} · {{ $a->analysis_type }}
    {{ __('Tenant') }} #{{ $a->tenant_id }} · {{ __('started') }} {{ \Illuminate\Support\Carbon::parse($a->started_at)->diffForHumans() }}
  • @endforeach
@else

{{ __('Analysis runtime (last 24h)') }}

{{ __('Avg') }}
{{ $runtimeAvg !== null ? $runtimeAvg.'s' : '—' }}
{{ __('p50') }}
{{ $runtimeP50 !== null ? $runtimeP50.'s' : '—' }}
{{ __('p95') }}
{{ $runtimeP95 !== null ? $runtimeP95.'s' : '—' }}

{{ __('Wall-clock time from the worker picking up the job to the analysis finishing. p95 above 180 s suggests the LLM is throttling or the prompt is too large for the chosen token budget.') }}

@endif {{-- Top tenants --}}

{{ __('Top tenants (last 24h)') }}

@if($topTenants->isEmpty())
{{ __('No analyses in the last 24 hours.') }}
@else
    @foreach($topTenants as $t)
  • {{ $t->name }}
    #{{ $t->tenant_id }}
    {{ number_format($t->n) }}
    {{ number_format($t->tok ?? 0) }} tk
  • @endforeach
@endif
{{-- ── FAILURES (analyses + jobs, side-by-side) ──────────────── --}}

{{ __('Recent analysis failures (last 24h)') }}

@if($recentAnalysisFailures->isEmpty())
{{ __('No analysis failures in the last 24 hours.') }}
@else
    @foreach($recentAnalysisFailures as $a)
  • #{{ $a->id }} · {{ $a->analysis_type }}
    {{ __('Tenant') }} #{{ $a->tenant_id }} · {{ \Illuminate\Support\Carbon::parse($a->finished_at)->diffForHumans() }}
    @if($a->error)
    {{ $a->error }}
    @endif
  • @endforeach
@endif

{{ __('Recent failed jobs') }}

{{ __('From the failed_jobs table — Laravel-level failures (timeouts, OOM, etc.).') }}

@if($recentFailures->isEmpty())
{{ __('No failed jobs.') }}
@else
    @foreach($recentFailures as $f) @php $excerpt = trim((string) $f->exception); $firstLine = strtok($excerpt, "\n"); @endphp
  • {{ $f->queue }} · {{ $f->connection }}
    {{ \Illuminate\Support\Carbon::parse($f->failed_at)->diffForHumans() }}
    {{ $firstLine ?: '—' }}
  • @endforeach
@endif
@endsection