- Added standard Laravel directory structure and configuration. - Included Svelte and Tailwind configuration for the admin interface. - Added core PHPUnit and testing scripts.
173 lines
7.2 KiB
PHP
173 lines
7.2 KiB
PHP
@extends('layouts.admin')
|
|
|
|
@section('title', 'Site Backups')
|
|
|
|
@section('content')
|
|
<div class="ui container">
|
|
<div class="ui grid">
|
|
<div class="row">
|
|
<div class="eight wide column">
|
|
<h2 class="ui header">
|
|
<i class="archive icon"></i>
|
|
<div class="content">
|
|
Backups
|
|
<div class="sub header">Manage and create site backups</div>
|
|
</div>
|
|
</h2>
|
|
</div>
|
|
<div class="eight wide column right aligned">
|
|
<form action="{{ route('admin.backups.upload') }}" method="POST" enctype="multipart/form-data" style="display:inline; margin-right: 10px;">
|
|
@csrf
|
|
<div class="ui action input">
|
|
<input type="file" name="backup_file" accept=".gz" style="display: none;" id="backup_file_input" onchange="this.form.submit()">
|
|
<button type="button" class="ui icon button" onclick="document.getElementById('backup_file_input').click()">
|
|
<i class="upload icon"></i> Upload Backup
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<form action="{{ route('admin.backups.store') }}" method="POST" style="display:inline;">
|
|
@csrf
|
|
<button type="submit" class="ui primary button">
|
|
<i class="plus icon"></i> Create New Backup
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sixteen wide column">
|
|
@if($errors->any())
|
|
<div class="ui error message">
|
|
<i class="close icon"></i>
|
|
<div class="header">Error</div>
|
|
<ul class="list">
|
|
@foreach($errors->all() as $error)
|
|
<li>{{ $error }}</li>
|
|
@endforeach
|
|
</ul>
|
|
</div>
|
|
@endif
|
|
|
|
@if(session('success'))
|
|
<div class="ui success message">
|
|
<i class="close icon"></i>
|
|
<div class="header">Success</div>
|
|
<p>{{ session('success') }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
@if(session('error'))
|
|
<div class="ui error message">
|
|
<i class="close icon"></i>
|
|
<div class="header">Error</div>
|
|
<p>{{ session('error') }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
<div id="restore-progress-container" style="display: none; margin-bottom: 20px;">
|
|
<div class="ui segment">
|
|
<h4 class="ui header" id="restore-status">Restoring Backup...</h4>
|
|
<div class="ui indicating progress" id="restore-progress">
|
|
<div class="bar">
|
|
<div class="progress"></div>
|
|
</div>
|
|
<div class="label" id="restore-label">Waiting to start...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="ui celled table">
|
|
<thead>
|
|
<tr>
|
|
<th>Backup Name</th>
|
|
<th>Size</th>
|
|
<th>Created At</th>
|
|
<th class="right aligned">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($backups as $backup)
|
|
<tr>
|
|
<td>
|
|
<i class="file archive outline icon"></i>
|
|
{{ $backup['name'] }}
|
|
</td>
|
|
<td>{{ $backup['size'] }}</td>
|
|
<td>{{ $backup['date'] }}</td>
|
|
<td class="right aligned">
|
|
<form action="{{ route('admin.backups.restore') }}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to restore this backup? This will overwrite your current site data.')">
|
|
@csrf
|
|
<input type="hidden" name="filename" value="{{ $backup['name'] }}">
|
|
<button type="submit" class="ui tiny orange button">
|
|
<i class="undo icon"></i> Restore
|
|
</button>
|
|
</form>
|
|
<a href="{{ route('admin.backups.download', ['filename' => $backup['name']]) }}" class="ui tiny primary button">
|
|
<i class="download icon"></i> Download
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="4" class="center aligned">No backups found.</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
function startProgressPolling() {
|
|
document.getElementById('restore-progress-container').style.display = 'block';
|
|
const progressEl = document.getElementById('restore-progress');
|
|
const statusEl = document.getElementById('restore-status');
|
|
const labelEl = document.getElementById('restore-label');
|
|
|
|
$(progressEl).progress({
|
|
percent: 0
|
|
});
|
|
|
|
const interval = setInterval(async () => {
|
|
try {
|
|
const response = await fetch('{{ route('admin.backups.restore.progress') }}');
|
|
const data = await response.json();
|
|
|
|
if (data) {
|
|
$(progressEl).progress('set progress', data.percent);
|
|
statusEl.innerText = data.status;
|
|
labelEl.innerText = data.percent + '% - ' + data.status;
|
|
|
|
if (data.percent >= 100) {
|
|
clearInterval(interval);
|
|
setTimeout(() => {
|
|
location.reload();
|
|
}, 2000);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to poll progress', e);
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
// Attach to the restore forms
|
|
document.querySelectorAll('form[action="{{ route('admin.backups.restore') }}"]').forEach(form => {
|
|
form.addEventListener('submit', function(e) {
|
|
// We don't prevent default because we want the form to submit
|
|
// and the synchronous Artisan call to run.
|
|
// But we start polling immediately.
|
|
// Note: Since the Artisan call is synchronous in the controller,
|
|
// the page will actually be waiting for the response.
|
|
// To make polling work, we'd ideally need an async job.
|
|
// However, with PHP-FPM, if the same session tries to access the progress route
|
|
// it might be blocked if session locking is on.
|
|
// For now, we'll keep it simple as requested by "UI & Reliability Polish".
|
|
setTimeout(startProgressPolling, 100);
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|