274 lines
12 KiB
PHP
274 lines
12 KiB
PHP
@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
|