Files
opentextsummaries/resources/views/admin/gutendex/dashboard.blade.php
T

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