gutendex integration, phase 6
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<h1>Gutendex Management Dashboard</h1>
|
||||
<p class="text-muted">Import and manage books from Project Gutenberg</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-muted">Total Books</h5>
|
||||
<h2 class="mb-0">{{ number_format($stats['total_books']) }}</h2>
|
||||
<small class="text-muted">{{ number_format($stats['gutendex_books']) }} from Gutendex</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-muted">Completed Summaries</h5>
|
||||
<h2 class="mb-0 text-success">{{ number_format($stats['completed_summaries']) }}</h2>
|
||||
<small class="text-muted">{{ $stats['completion_rate'] }}% completion rate</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-muted">Pending/Processing</h5>
|
||||
<h2 class="mb-0 text-warning">{{ number_format($stats['pending_summaries'] + $stats['processing_summaries']) }}</h2>
|
||||
<small class="text-muted">{{ number_format($stats['pending_summaries']) }} pending, {{ number_format($stats['processing_summaries']) }} processing</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-muted">Failed</h5>
|
||||
<h2 class="mb-0 text-danger">{{ number_format($stats['failed_summaries']) }}</h2>
|
||||
<small class="text-muted">Need attention</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Import Form -->
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Import Books from Gutendex</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="import-form">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label for="language" class="form-label">Language</label>
|
||||
<select class="form-select" id="language" name="language">
|
||||
<option value="en" selected>English</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="it">Italian</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="limit" class="form-label">Number of Books</label>
|
||||
<input type="number" class="form-control" id="limit" name="limit" value="50" min="1" max="500">
|
||||
<small class="form-text text-muted">Maximum 500 books per import</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="page" class="form-label">Starting Page</label>
|
||||
<input type="number" class="form-control" id="page" name="page" value="1" min="1">
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="generate_summaries" name="generate_summaries" checked>
|
||||
<label class="form-check-label" for="generate_summaries">
|
||||
Generate summaries automatically
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="import-btn">
|
||||
<i class="fas fa-download"></i> Start Import
|
||||
</button>
|
||||
</form>
|
||||
<div id="import-status" class="mt-3" style="display: none;">
|
||||
<div class="alert alert-info">
|
||||
<strong>Import started!</strong> Jobs have been dispatched to the queue.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Quick Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a href="{{ route('admin.gutendex.status') }}" class="btn btn-info btn-sm mb-2 w-100">
|
||||
<i class="fas fa-chart-line"></i> View Processing Status
|
||||
</a>
|
||||
<button class="btn btn-warning btn-sm mb-2 w-100" onclick="retryFailed()">
|
||||
<i class="fas fa-redo"></i> Retry Failed Summaries
|
||||
</button>
|
||||
<a href="/horizon" class="btn btn-secondary btn-sm w-100" target="_blank">
|
||||
<i class="fas fa-tachometer-alt"></i> Queue Dashboard (if Horizon installed)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="col-md-6">
|
||||
<!-- Recent Imports -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Recent Imports</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if(count($recentImports) > 0)
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Author</th>
|
||||
<th>Category</th>
|
||||
<th>Imported</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($recentImports as $book)
|
||||
<tr>
|
||||
<td>{{ Str::limit($book['title'], 30) }}</td>
|
||||
<td>{{ Str::limit($book['author'], 20) }}</td>
|
||||
<td><span class="badge bg-secondary">{{ $book['category']['name'] ?? 'N/A' }}</span></td>
|
||||
<td><small>{{ \Carbon\Carbon::parse($book['created_at'])->diffForHumans() }}</small></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-muted">No recent imports</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Failures -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Recent Failures</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if(count($recentFailures) > 0)
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Book</th>
|
||||
<th>Error</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($recentFailures as $summary)
|
||||
<tr>
|
||||
<td>{{ Str::limit($summary['book']['title'] ?? 'Unknown', 25) }}</td>
|
||||
<td><small class="text-danger">{{ Str::limit($summary['processing_error'] ?? 'Unknown error', 30) }}</small></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-warning" onclick="retryBook({{ $summary['book_id'] }})">
|
||||
<i class="fas fa-redo"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-muted">No recent failures</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('import-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('import-btn');
|
||||
const status = document.getElementById('import-status');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Starting...';
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const data = {
|
||||
language: formData.get('language'),
|
||||
limit: parseInt(formData.get('limit')),
|
||||
page: parseInt(formData.get('page')),
|
||||
generate_summaries: formData.get('generate_summaries') ? 1 : 0,
|
||||
_token: formData.get('_token')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('{{ route('admin.gutendex.import') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': data._token
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
status.style.display = 'block';
|
||||
status.innerHTML = `<div class="alert alert-success"><strong>Success!</strong> ${result.message}</div>`;
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
} else {
|
||||
status.style.display = 'block';
|
||||
status.innerHTML = `<div class="alert alert-danger"><strong>Error!</strong> ${result.message || 'Import failed'}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
status.style.display = 'block';
|
||||
status.innerHTML = `<div class="alert alert-danger"><strong>Error!</strong> ${error.message}</div>`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-download"></i> Start Import';
|
||||
}
|
||||
});
|
||||
|
||||
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: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Retry job dispatched successfully!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Failed to dispatch retry job: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function retryFailed() {
|
||||
if (confirm('This will retry all failed summaries. Continue?')) {
|
||||
alert('Feature coming soon! Use CLI: php artisan summary:generate --failed');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,269 @@
|
||||
@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
|
||||
Reference in New Issue
Block a user