270 lines
11 KiB
PHP
270 lines
11 KiB
PHP
@extends('layouts.app')
|
|||
|
|
|
||
|
|
@section('content')
|
||
|
|
<div class="container">
|
||
|
|
<div class="row mb-4">
|
||
|
|
<div class="col-md-12">
|
||
|
|
<div class="d-flex justify-content-between align-items-center">
|
||
|
|
<div>
|
||
|
|
<h1>Processing Status</h1>
|
||
|
|
<p class="text-muted">Monitor summary generation progress</p>
|
||
|
|
</div>
|
||
|
|
<a href="{{ route('admin.gutendex.dashboard') }}" class="btn btn-secondary">
|
||
|
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Overall Statistics -->
|
||
|
|
<div class="row mb-4">
|
||
|
|
<div class="col-md-3">
|
||
|
|
<div class="card text-center">
|
||
|
|
<div class="card-body">
|
||
|
|
<h6 class="text-muted">Total Summaries</h6>
|
||
|
|
<h3>{{ number_format($stats['total_summaries']) }}</h3>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-3">
|
||
|
|
<div class="card text-center border-success">
|
||
|
|
<div class="card-body">
|
||
|
|
<h6 class="text-success">Completed</h6>
|
||
|
|
<h3 class="text-success">{{ number_format($stats['completed_summaries']) }}</h3>
|
||
|
|
<small>{{ $stats['completion_rate'] }}%</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-3">
|
||
|
|
<div class="card text-center border-warning">
|
||
|
|
<div class="card-body">
|
||
|
|
<h6 class="text-warning">Pending/Processing</h6>
|
||
|
|
<h3 class="text-warning">{{ number_format($stats['pending_summaries'] + $stats['processing_summaries']) }}</h3>
|
||
|
|
<small>{{ number_format($stats['pending_summaries']) }} + {{ number_format($stats['processing_summaries']) }}</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-3">
|
||
|
|
<div class="card text-center border-danger">
|
||
|
|
<div class="card-body">
|
||
|
|
<h6 class="text-danger">Failed</h6>
|
||
|
|
<h3 class="text-danger">{{ number_format($stats['failed_summaries']) }}</h3>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Queue Status -->
|
||
|
|
<div class="row mb-4">
|
||
|
|
<div class="col-md-12">
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<h5 class="mb-0">Queue Status</h5>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-md-6">
|
||
|
|
<h6>Pending Jobs</h6>
|
||
|
|
<h2>{{ number_format($queueStats['pending_jobs']) }}</h2>
|
||
|
|
@if($queueStats['pending_jobs'] > 0)
|
||
|
|
<small class="text-muted">Jobs waiting to be processed</small>
|
||
|
|
@else
|
||
|
|
<small class="text-success">Queue is empty</small>
|
||
|
|
@endif
|
||
|
|
</div>
|
||
|
|
<div class="col-md-6">
|
||
|
|
<h6>Failed Jobs</h6>
|
||
|
|
<h2 class="text-danger">{{ number_format($queueStats['failed_jobs']) }}</h2>
|
||
|
|
@if($queueStats['failed_jobs'] > 0)
|
||
|
|
<small class="text-danger">Requires attention</small>
|
||
|
|
@else
|
||
|
|
<small class="text-success">No failed jobs</small>
|
||
|
|
@endif
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<hr>
|
||
|
|
<button class="btn btn-primary btn-sm" onclick="location.reload()">
|
||
|
|
<i class="fas fa-sync"></i> Refresh Status
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Currently Processing -->
|
||
|
|
@if($processing->count() > 0)
|
||
|
|
<div class="row mb-4">
|
||
|
|
<div class="col-md-12">
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header bg-primary text-white">
|
||
|
|
<h5 class="mb-0"><i class="fas fa-spinner fa-spin"></i> Currently Processing ({{ $processing->count() }})</h5>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="table-responsive">
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Book ID</th>
|
||
|
|
<th>Title</th>
|
||
|
|
<th>Author</th>
|
||
|
|
<th>Status</th>
|
||
|
|
<th>Started</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
@foreach($processing as $summary)
|
||
|
|
<tr>
|
||
|
|
<td>{{ $summary->book_id }}</td>
|
||
|
|
<td>{{ Str::limit($summary->book->title ?? 'Unknown', 40) }}</td>
|
||
|
|
<td>{{ Str::limit($summary->book->author ?? 'Unknown', 30) }}</td>
|
||
|
|
<td><span class="badge bg-warning">Processing</span></td>
|
||
|
|
<td>{{ $summary->updated_at->diffForHumans() }}</td>
|
||
|
|
</tr>
|
||
|
|
@endforeach
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
<!-- Pending Summaries -->
|
||
|
|
@if($pending->count() > 0)
|
||
|
|
<div class="row mb-4">
|
||
|
|
<div class="col-md-12">
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<h5 class="mb-0"><i class="fas fa-clock"></i> Pending Summaries ({{ $pending->count() }})</h5>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="table-responsive">
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Book ID</th>
|
||
|
|
<th>Title</th>
|
||
|
|
<th>Author</th>
|
||
|
|
<th>Created</th>
|
||
|
|
<th>Action</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
@foreach($pending as $summary)
|
||
|
|
<tr>
|
||
|
|
<td>{{ $summary->book_id }}</td>
|
||
|
|
<td>{{ Str::limit($summary->book->title ?? 'Unknown', 40) }}</td>
|
||
|
|
<td>{{ Str::limit($summary->book->author ?? 'Unknown', 30) }}</td>
|
||
|
|
<td>{{ $summary->created_at->diffForHumans() }}</td>
|
||
|
|
<td>
|
||
|
|
<button class="btn btn-sm btn-primary" onclick="retryBook({{ $summary->book_id }})">
|
||
|
|
<i class="fas fa-play"></i> Process
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
@endforeach
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
<!-- Failed Summaries -->
|
||
|
|
@if($failed->count() > 0)
|
||
|
|
<div class="row mb-4">
|
||
|
|
<div class="col-md-12">
|
||
|
|
<div class="card border-danger">
|
||
|
|
<div class="card-header bg-danger text-white">
|
||
|
|
<h5 class="mb-0"><i class="fas fa-exclamation-triangle"></i> Failed Summaries ({{ $failed->count() }})</h5>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="table-responsive">
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Book ID</th>
|
||
|
|
<th>Title</th>
|
||
|
|
<th>Error</th>
|
||
|
|
<th>Failed At</th>
|
||
|
|
<th>Action</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
@foreach($failed as $summary)
|
||
|
|
<tr>
|
||
|
|
<td>{{ $summary->book_id }}</td>
|
||
|
|
<td>{{ Str::limit($summary->book->title ?? 'Unknown', 30) }}</td>
|
||
|
|
<td><small class="text-danger">{{ Str::limit($summary->processing_error ?? 'Unknown error', 60) }}</small></td>
|
||
|
|
<td>{{ $summary->updated_at->diffForHumans() }}</td>
|
||
|
|
<td>
|
||
|
|
<button class="btn btn-sm btn-warning" onclick="retryBook({{ $summary->book_id }})">
|
||
|
|
<i class="fas fa-redo"></i> Retry
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
@endforeach
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
<!-- No Activity -->
|
||
|
|
@if($processing->count() == 0 && $pending->count() == 0 && $failed->count() == 0)
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-md-12">
|
||
|
|
<div class="alert alert-info">
|
||
|
|
<h5><i class="fas fa-info-circle"></i> No Active Processing</h5>
|
||
|
|
<p>All summaries are either completed or no jobs are currently queued.</p>
|
||
|
|
<a href="{{ route('admin.gutendex.dashboard') }}" class="btn btn-primary">Start New Import</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
async function retryBook(bookId) {
|
||
|
|
if (!confirm('Retry summary generation for this book?')) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/admin/gutendex/retry/${bookId}`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
alert('✓ Retry job dispatched successfully!');
|
||
|
|
location.reload();
|
||
|
|
} else {
|
||
|
|
alert('✗ Failed: ' + result.message);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
alert('✗ Error: ' + error.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auto-refresh every 30 seconds
|
||
|
|
let autoRefresh = setInterval(() => {
|
||
|
|
location.reload();
|
||
|
|
}, 30000);
|
||
|
|
|
||
|
|
// Stop auto-refresh when user interacts
|
||
|
|
document.addEventListener('click', () => {
|
||
|
|
clearInterval(autoRefresh);
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
@endsection
|