Files

851 lines
56 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<x-app-layout>
@push('styles')
<style>
.status-widget-dark {
border-radius: 24px;
background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
padding: 1.5rem;
color: white;
margin-bottom: 1.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
}
.btn-premium-action {
border-radius: 100px;
padding: 10px 24px;
font-weight: 700;
transition: all 0.3s ease;
}
.table-premium thead th {
background: #f8fafc;
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.05em;
color: #64748b;
padding: 1.25rem 1rem;
border-bottom: 1px solid #f1f5f9;
}
.table-premium tbody td {
padding: 1rem;
border-bottom: 1px solid #f1f5f9;
}
.custom-switch-premium .form-check-input {
width: 3rem;
height: 1.5rem;
cursor: pointer;
}
@keyframes pulse-danger {
0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(220, 53, 69, 0); }
100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); }
}
.pulse-danger {
animation: pulse-danger 2s infinite;
}
.animate-spin {
animation: spin 1s infinite linear;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
@endpush
<div class="container-fluid pb-5">
{{-- Premium Page Header --}}
<div class="d-flex align-items-center justify-content-between mb-4 animate__animated animate__fadeIn">
<div>
<h4 class="fw-bold mb-1" style="font-family: 'Outfit', sans-serif; letter-spacing: -0.5px;">
{{ __('Backup & Storage') }}
</h4>
<p class="text-muted small mb-0">
{{ __('Securely manage your system archives and cloud synchronization.') }}
</p>
</div>
<div class="d-flex gap-2">
<button type="button"
class="btn btn-outline-dark btn-premium-action shadow-sm d-flex align-items-center gap-2"
data-bs-toggle="modal" data-bs-target="#backupDocsModal">
<i class="bi bi-book"></i> {{ __('Documentation') }}
</button>
@can('manage backup and storage')
<button type="button"
class="btn btn-primary btn-premium-action shadow-sm d-flex align-items-center gap-2"
id="btn-create-backup">
<i class="bi bi-plus-lg"></i> {{ __('Instant Backup') }}
</button>
@endcan
</div>
</div>
<div class="row gx-4">
{{-- Left Side: Control Center --}}
<div class="col-lg-4 animate__animated animate__fadeIn">
{{-- Storage Health Widget (Dark Premium) --}}
<div id="storage-health-widget" class="status-widget-dark" style="display:none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-2">
<div class="bg-white bg-opacity-10 rounded-circle p-2">
<i class="bi bi-cloud-check text-info"></i>
</div>
<span class="small fw-bold opacity-75 text-uppercase tracking-wider"
id="health-label">Storage</span>
</div>
<span class="badge rounded-pill bg-info text-dark fw-bold px-3 py-2" id="health-status"
style="font-size: 10px;">Checking...</span>
</div>
<h2 class="mb-1 fw-bold" id="health-used" style="font-family: 'Outfit', sans-serif;">0</h2>
<p class="small opacity-50 mb-4" id="health-total-label">of <span id="health-total">0</span> used
</p>
<div class="progress bg-white bg-opacity-10 mb-2" style="height: 8px; border-radius: 10px;"
id="health-progress-wrapper">
<div class="progress-bar bg-info" id="health-progress-bar" role="progressbar"
style="width: 0%; border-radius: 10px;"></div>
</div>
{{-- Requirements Alert --}}
<div id="requirements-alert"
class="mt-3 p-2 rounded-3 bg-danger bg-opacity-25 border border-danger border-opacity-25 small"
style="display:none;">
<i class="bi bi-exclamation-triangle me-1"></i> <span id="req-msg">Check reqs</span>
</div>
</div>
<form id="backupConfigForm" action="{{ route('system-config.update') }}" method="POST"
autocomplete="off" class="ajax-form" data-reset="false">
@csrf
@method('PUT')
{{-- Automation Card --}}
<div class="card adminuiux-card mb-4">
<div class="card-body">
<h6 class="fw-bold mb-4 d-flex align-items-center gap-2">
<i class="bi bi-clock-history text-primary"></i>
{{ __('Automation Settings') }}
</h6>
<div
class="form-check form-switch custom-switch-premium mb-4 p-0 d-flex align-items-center justify-content-between">
<label class="form-check-label fw-semibold text-dark"
for="backup_db_enabled">{{ __('AUTO BACKUP') }}</label>
<input class="form-check-input ms-0" type="checkbox" role="switch"
id="backup_db_enabled" name="backup_db_enabled" value="1"
@checked(old('backup_db_enabled', $settings['backup_db_enabled'] ?? true))>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">{{ __('Backup Frequency') }}</label>
<select class="form-select" name="backup_db_frequency">
<option value="hourly" @selected(($settings['backup_db_frequency'] ?? '') == 'hourly')>{{ __('Hourly') }}</option>
<option value="daily" @selected(($settings['backup_db_frequency'] ?? 'daily') == 'daily')>{{ __('Daily') }}</option>
<option value="weekly" @selected(($settings['backup_db_frequency'] ?? '') == 'weekly')>{{ __('Weekly') }}</option>
<option value="monthly" @selected(($settings['backup_db_frequency'] ?? '') == 'monthly')>{{ __('Monthly') }}</option>
</select>
</div>
<div class="row g-2">
<div class="col-6">
<label class="form-label fw-semibold">{{ __('Execution Time') }}</label>
<input type="time" class="form-control" name="backup_db_time"
value="{{ $settings['backup_db_time'] ?? '02:00' }}">
</div>
<div class="col-6">
<label class="form-label fw-semibold">{{ __('Retention (Days)') }}</label>
<input type="number" class="form-control text-center" name="backup_db_retention"
value="{{ $settings['backup_db_retention'] ?? 7 }}">
</div>
</div>
</div>
</div>
{{-- Storage Destination Card --}}
<div class="card adminuiux-card mb-4">
<div class="card-body">
<h6 class="fw-bold mb-4 d-flex align-items-center gap-2">
<i class="bi bi-hdd-network text-primary"></i>
{{ __('Storage Target') }}
</h6>
<div class="mb-4">
<label class="form-label fw-semibold mb-2">{{ __('Primary Driver') }}</label>
<select class="form-select" name="backup_db_driver" id="backup_db_driver">
<option value="local" @selected(($settings['backup_db_driver'] ?? 'local') == 'local')>Local Filesystem</option>
<option value="gdrive" @selected(($settings['backup_db_driver'] ?? '') == 'gdrive')>
Google Drive</option>
</select>
</div>
{{-- Cloud Fields Wrapper --}}
<div id="cloud_config_wrapper"
style="display: {{ ($settings['backup_db_driver'] ?? '') == 'gdrive' ? 'block' : 'none' }}">
<div class="p-3 rounded-4 bg-light border border-opacity-10 mb-4">
{{-- Google Drive --}}
<div id="gdrive_fields"
style="display: {{ ($settings['backup_db_driver'] ?? '') == 'gdrive' ? 'block' : 'none' }}">
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-google text-primary"></i>
<h6 class="fw-semibold mb-0 small">{{ __('Google Cloud Auth') }}</h6>
</div>
<div class="mb-2">
<input type="text"
class="form-control form-control-sm rounded-pill px-3 bg-white"
name="gdrive_client_id"
value="{{ $settings['gdrive_client_id'] ?? '' }}"
placeholder="Client ID">
</div>
<div class="input-group input-group-sm mb-2">
<input type="password"
class="form-control border-end-0 rounded-start-pill px-3 bg-white"
name="gdrive_client_secret"
value="{{ $settings['gdrive_client_secret'] ?? '' }}"
placeholder="Client Secret">
<button
class="btn btn-outline-secondary bg-white border-start-0 password-toggle rounded-end-pill pe-3"
type="button" style="border-color: #dee2e6;">
<i class="bi bi-eye text-secondary"></i>
</button>
</div>
<div class="input-group input-group-sm mb-2">
<input type="password"
class="form-control border-0 rounded-start-pill px-3 bg-white"
name="gdrive_refresh_token"
value="{{ $settings['gdrive_refresh_token'] ?? '' }}"
placeholder="Refresh Token">
<button class="btn btn-outline-secondary bg-white border-0 password-toggle"
type="button">
<i class="bi bi-eye text-secondary"></i>
</button>
<a href="{{ route('backup-restore.google-auth') }}"
class="btn btn-dark rounded-end-pill px-3" id="btn-gdrive-auth">
<i class="bi bi-key"></i>
</a>
</div>
<div class="input-group input-group-sm">
<input type="text"
class="form-control border-0 rounded-start-pill px-3 bg-white"
name="gdrive_folder"
value="{{ $settings['gdrive_folder'] ?? 'LaravelBackups' }}"
placeholder="Folder Name">
<button type="button"
class="btn btn-primary rounded-end-pill px-3 btn-test-connection"
data-driver="gdrive">
<i class="bi bi-broadcast"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
@can('manage backup and storage')
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary rounded-pill px-4 shadow-sm">
{{ __('Save Configuration') }}
</button>
</div>
@endcan
</form>
</div>
{{-- Right Side: History (Col-8) --}}
<div class="col-lg-8 animate__animated animate__fadeIn">
<div class="card adminuiux-card h-100">
<div class="card-body p-0">
<div class="p-4 d-flex justify-content-between align-items-center border-bottom bg-white sticky-top"
style="z-index: 10; border-top-left-radius: 20px; border-top-right-radius: 20px;">
<h6 class="fw-bold mb-0 d-flex align-items-center gap-2">
<i class="bi bi-collection-play text-primary"></i>
{{ __('Archive Inventory') }}
</h6>
<button type="button" class="btn btn-sm btn-light rounded-pill px-3"
id="btn-refresh-backups">
<i class="bi bi-arrow-clockwise me-1"></i> {{ __('Refresh List') }}
</button>
</div>
<div class="table-responsive">
<table class="table table-premium align-middle mb-0" id="backup-list-table">
<thead>
<tr>
<th class="ps-4">{{ __('Archive Name') }}</th>
<th>{{ __('Status') }}</th>
<th>{{ __('Timestamp') }}</th>
<th class="text-end pe-4">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody>
<tr id="backup-loading">
<td colspan="4" class="text-center py-5">
<div class="spinner-grow spinner-grow-sm text-primary me-2"></div>
<span
class="text-muted small fw-bold">{{ __('Scanning Archives...') }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{{-- ====================================================================
DOCUMENTATION (MODAL)
==================================================================== --}}
<div class="modal fade" id="backupDocsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-fullscreen-lg-down modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content rounded-4 border-0 shadow-lg overflow-hidden">
<div class="modal-header p-0 border-0" style="background:#0f172a;">
<div class="d-flex w-100 align-items-center justify-content-between p-4 p-lg-5 text-white">
<div>
<div class="extra-small opacity-50 fw-bold mb-2" style="letter-spacing:1.5px;">REFERENCE GUIDE</div>
<h3 class="fw-black mb-2" style="letter-spacing:-1px;">{{ __('Backup & Restore Documentation') }}</h3>
<p class="mb-0 opacity-75" style="font-size:.9rem;line-height:1.7;max-width:720px;">
Everything you need to know about creating, scheduling, restoring, and securing your system archives.
<span class="text-warning fw-bold">Read this first</span> restoring overwrites your live data.
</p>
</div>
<div class="d-flex align-items-start gap-3">
<i class="bi bi-shield-fill-check display-3 opacity-25 d-none d-md-inline"></i>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
<div class="modal-body p-0">
<div class="p-4 p-lg-5">
{{-- DANGER NOTICE --}}
<div class="d-flex align-items-start gap-3 p-3 rounded-4 mb-5" style="background:#fef2f2;border:2px solid #fecaca;">
<i class="bi bi-exclamation-triangle-fill text-danger fs-2"></i>
<div>
<div class="fw-black text-danger mb-1">CRITICAL Restore is destructive</div>
<div class="small text-dark" style="line-height:1.7;">
Restoring a backup <span class="fw-bold">overwrites every table</span> in your current database with the data from the archive.
All records created since that backup was taken will be <span class="fw-bold text-danger">permanently lost</span>.
Always create a fresh <span class="fw-bold">Instant Backup</span> immediately before restoring, so you have a return path.
</div>
</div>
</div>
{{-- WORKFLOW STEPS --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-diagram-3 text-primary me-1"></i>How a backup is created
</h6>
<div class="row g-3 mb-5">
@php
$steps = [
['icon'=>'bi-database', 'color'=>'#3b82f6', 'title'=>'1. Snapshot', 'desc'=>'A full SQL dump of every table is generated via <code>mysqldump</code> / <code>pg_dump</code> at the moment you click Instant Backup or when the scheduler fires.'],
['icon'=>'bi-file-zip', 'color'=>'#8b5cf6', 'title'=>'2. Compress', 'desc'=>'The SQL dump is gzip-compressed into a single <code>.sql.gz</code> archive named with timestamp + driver tag.'],
['icon'=>'bi-hdd-network', 'color'=>'#22c55e', 'title'=>'3. Transfer', 'desc'=>'Archive is written to the configured driver. <span class="fw-bold">Local</span> saves to <code>storage/app/backups/</code>. <span class="fw-bold">Google Drive</span> uploads via OAuth refresh token.'],
['icon'=>'bi-clipboard-check','color'=>'#f59e0b', 'title'=>'4. Verify', 'desc'=>'File size and SHA-256 checksum are recorded. The Archive Inventory table reflects the new row immediately.'],
['icon'=>'bi-trash3', 'color'=>'#ef4444', 'title'=>'5. Prune', 'desc'=>'Archives older than the configured retention (days) are auto-deleted on the next scheduled run. Local + cloud are pruned independently.'],
];
@endphp
@foreach($steps as $s)
<div class="col-md-6 col-lg-4">
<div class="p-3 rounded-4 h-100" style="background:#f8fafc;border:1px solid #e2e8f0;">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="rounded-3 d-flex align-items-center justify-content-center flex-shrink-0"
style="background:{{ $s['color'] }}1a;color:{{ $s['color'] }};width:36px;height:36px;">
<i class="{{ $s['icon'] }}"></i>
</div>
<div class="fw-bold text-dark">{{ $s['title'] }}</div>
</div>
<div class="small text-muted" style="line-height:1.6;">{!! $s['desc'] !!}</div>
</div>
</div>
@endforeach
</div>
{{-- STORAGE DRIVERS --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-hdd-stack text-info me-1"></i>Storage drivers comparison
</h6>
<div class="table-responsive mb-5">
<table class="table table-hover align-middle mb-0" style="background:#f8fafc;border-radius:12px;overflow:hidden;">
<thead style="background:#e2e8f0;">
<tr>
<th class="ps-3">DRIVER</th>
<th>WHERE</th>
<th>SETUP</th>
<th>BEST FOR</th>
<th>RISK IF SERVER DIES</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-3 fw-bold text-dark">
<i class="bi bi-hdd text-secondary me-1"></i>Local Filesystem
</td>
<td class="small text-muted"><code>storage/app/backups/</code></td>
<td class="small"><span class="badge bg-success-subtle text-success rounded-pill">Zero config</span></td>
<td class="small text-muted">Quick recovery, small datasets, dev environments</td>
<td class="small text-danger fw-bold">Total loss backups on same disk</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">
<i class="bi bi-google text-primary me-1"></i>Google Drive
</td>
<td class="small text-muted">OAuth folder (default: <code>LaravelBackups</code>)</td>
<td class="small"><span class="badge bg-warning-subtle text-warning rounded-pill">Client ID + Secret + Refresh Token</span></td>
<td class="small text-muted">Off-site redundancy, production, disaster recovery</td>
<td class="small text-success fw-bold">Safe off-server copy</td>
</tr>
</tbody>
</table>
</div>
{{-- GOOGLE DRIVE SETUP --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-google text-primary me-1"></i>Google Drive setup · Step-by-step
</h6>
<div class="row g-3 mb-5">
<div class="col-md-6">
<div class="p-3 rounded-4 h-100" style="background:#eff6ff;border:1px solid #bfdbfe;">
<div class="fw-bold text-primary mb-2">1. Create OAuth client</div>
<ol class="small text-muted mb-0" style="line-height:1.8;padding-left:1.2rem;">
<li>Go to <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a></li>
<li>Create a project enable <span class="fw-bold">Google Drive API</span></li>
<li>OAuth consent screen External fill app name</li>
<li>Credentials Create OAuth client ID Web application</li>
<li>Add redirect URI: <code>{{ url('/backup-restore/google-callback') }}</code></li>
<li>Copy <span class="fw-bold">Client ID</span> &amp; <span class="fw-bold">Client Secret</span></li>
</ol>
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded-4 h-100" style="background:#eff6ff;border:1px solid #bfdbfe;">
<div class="fw-bold text-primary mb-2">2. Link this app</div>
<ol class="small text-muted mb-0" style="line-height:1.8;padding-left:1.2rem;">
<li>Paste Client ID + Secret in the form on the left</li>
<li>Click the <i class="bi bi-key"></i> key button next to Refresh Token</li>
<li>Authorize on the Google consent screen</li>
<li>Refresh token is auto-filled on return</li>
<li>Click <i class="bi bi-broadcast"></i> to test connection</li>
<li>Save configuration</li>
</ol>
</div>
</div>
</div>
{{-- SCHEDULING --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-clock-history text-warning me-1"></i>Automation &amp; scheduling
</h6>
<div class="row g-3 mb-5">
<div class="col-md-3">
<div class="p-3 rounded-4 h-100" style="background:#fff;border:1px solid #e2e8f0;">
<div class="fw-bold text-dark mb-1">Frequency</div>
<div class="small text-muted">Hourly / Daily / Weekly / Monthly. The Laravel scheduler must be running (<code>cron</code> calling <code>php artisan schedule:run</code> every minute).</div>
</div>
</div>
<div class="col-md-3">
<div class="p-3 rounded-4 h-100" style="background:#fff;border:1px solid #e2e8f0;">
<div class="fw-bold text-dark mb-1">Execution Time</div>
<div class="small text-muted">24-hour format (default <code>02:00</code>). Pick low-traffic hours backup briefly locks tables.</div>
</div>
</div>
<div class="col-md-3">
<div class="p-3 rounded-4 h-100" style="background:#fff;border:1px solid #e2e8f0;">
<div class="fw-bold text-dark mb-1">Retention</div>
<div class="small text-muted">Days to keep. Older archives auto-pruned. Default <code>7</code>. Set to <code>0</code> to keep forever (not recommended).</div>
</div>
</div>
<div class="col-md-3">
<div class="p-3 rounded-4 h-100" style="background:#fff;border:1px solid #e2e8f0;">
<div class="fw-bold text-dark mb-1">Auto Backup toggle</div>
<div class="small text-muted">Master switch. When OFF, only manual <span class="fw-bold">Instant Backup</span> runs.</div>
</div>
</div>
</div>
{{-- RESTORE PROCEDURE --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-arrow-counterclockwise text-danger me-1"></i>How to restore safely
</h6>
<div class="p-4 rounded-4 mb-5" style="background:#0f172a;color:#e2e8f0;">
<pre class="mb-0" style="font-family:'Fira Code',monospace;font-size:.78rem;line-height:2;white-space:pre-wrap;color:#cbd5e1;">
<span style="color:#22c55e;">RECOMMENDED RESTORE SEQUENCE:</span>
<span style="color:#fbbf24;">[1]</span> Click <span style="color:#22c55e;">Instant Backup</span> NOW creates a return path
<span style="color:#fbbf24;">[2]</span> Enable Maintenance Mode (so users can't write data mid-restore)
<span style="color:#fbbf24;">[3]</span> Open Archive Inventory → identify the archive you want
<span style="color:#fbbf24;">[4]</span> Click the row action menu → <span style="color:#ef4444;">Restore</span>
<span style="color:#fbbf24;">[5]</span> Confirm the dialog (read it carefully — points of no return)
<span style="color:#fbbf24;">[6]</span> Wait for the success toast (do NOT navigate away)
<span style="color:#fbbf24;">[7]</span> Verify data integrity on a couple of critical tables
<span style="color:#fbbf24;">[8]</span> Disable Maintenance Mode
<span style="color:#94a3b8;"># If something looks wrong after restore:
# → Use the safety backup from step [1] to roll back</span></pre>
</div>
{{-- TROUBLESHOOTING --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-wrench-adjustable-circle text-secondary me-1"></i>Troubleshooting
</h6>
<div class="table-responsive mb-5">
<table class="table table-hover align-middle mb-0" style="background:#f8fafc;border-radius:12px;overflow:hidden;">
<thead style="background:#e2e8f0;">
<tr>
<th class="ps-3">SYMPTOM</th>
<th>LIKELY CAUSE</th>
<th>FIX</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-3 fw-bold text-dark">Backup button does nothing</td>
<td class="small text-muted"><code>storage/app/backups/</code> not writable</td>
<td class="small text-muted"><code>chmod -R 775 storage/</code></td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">"Requirements not met" alert</td>
<td class="small text-muted"><code>mysqldump</code> / <code>pg_dump</code> not in PATH</td>
<td class="small text-muted">Install DB client tools on the server</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">Scheduled backup never runs</td>
<td class="small text-muted">Laravel scheduler cron not registered</td>
<td class="small text-muted">Add <code>* * * * * php artisan schedule:run</code> to crontab</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">Google Drive upload fails</td>
<td class="small text-muted">Refresh token expired or revoked</td>
<td class="small text-muted">Re-authorize via the <i class="bi bi-key"></i> key button</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">Disk fills up rapidly</td>
<td class="small text-muted">Retention too high or hourly schedule on large DB</td>
<td class="small text-muted">Lower retention, switch to daily, prune old archives manually</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">Restore hangs / times out</td>
<td class="small text-muted">Large archive, low PHP <code>max_execution_time</code></td>
<td class="small text-muted">Run restore via CLI: <code>php artisan backup:restore {file}</code></td>
</tr>
</tbody>
</table>
</div>
{{-- BEST PRACTICES --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-stars text-success me-1"></i>Best practices
</h6>
<div class="row g-2 mb-5">
@php
$tips = [
['icon'=>'bi-cloud-arrow-up', 'title'=>'Use 3-2-1 rule', 'desc'=>'<span class="fw-bold">3</span> copies of data, on <span class="fw-bold">2</span> different storage types, with <span class="fw-bold">1</span> off-site (Google Drive).'],
['icon'=>'bi-check2-circle', 'title'=>'Test your restores', 'desc'=>'A backup you have never restored is just hope, not insurance. Test in staging every quarter.'],
['icon'=>'bi-clock', 'title'=>'Off-hours schedule', 'desc'=>'Pick a time when traffic is lowest (typically 02:0004:00 local) to minimise lock contention.'],
['icon'=>'bi-shield-lock', 'title'=>'Restrict access', 'desc'=>'Only grant <code>manage backup and storage</code> to senior admins. Restore is one click away from catastrophe.'],
['icon'=>'bi-bar-chart-line', 'title'=>'Monitor storage health', 'desc'=>'Watch the dark widget in the sidebar when it turns red, prune or expand storage.'],
['icon'=>'bi-clipboard-pulse', 'title'=>'Document recovery plan', 'desc'=>'Write down (outside this system) the exact steps to restore + who has the Google account credentials.'],
];
@endphp
@foreach($tips as $tip)
<div class="col-md-6">
<div class="d-flex align-items-start gap-2 p-3 rounded-3" style="background:#f0fdf4;border:1px solid #bbf7d0;">
<i class="{{ $tip['icon'] }} text-success fs-5 mt-1"></i>
<div>
<div class="fw-bold text-dark mb-1" style="font-size:.85rem;">{{ $tip['title'] }}</div>
<div class="small text-muted" style="line-height:1.6;">{!! $tip['desc'] !!}</div>
</div>
</div>
</div>
@endforeach
</div>
{{-- PERMISSIONS --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-person-badge text-secondary me-1"></i>Permissions
</h6>
<div class="row g-2 mb-4">
<div class="col-md-6">
<div class="p-3 rounded-4" style="background:#f0fdf4;border:1px solid #bbf7d0;">
<div class="fw-bold text-success mb-1"><code class="text-success">view backup and storage</code></div>
<div class="small text-muted">Read-only access. Can see archive list and storage health but cannot create / restore / delete.</div>
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded-4" style="background:#fef2f2;border:1px solid #fecaca;">
<div class="fw-bold text-danger mb-1"><code class="text-danger">manage backup and storage</code></div>
<div class="small text-muted">Full control: create, restore, delete archives + change driver config + edit retention. <span class="fw-bold">Restrict tightly.</span></div>
</div>
</div>
</div>
{{-- FOOTER NOTE --}}
<div class="d-flex align-items-center gap-3 p-3 rounded-4" style="background:#fef3c7;border:1px solid #fde68a;">
<i class="bi bi-info-circle-fill text-warning fs-3"></i>
<div>
<div class="fw-bold text-dark mb-1" style="font-size:.85rem;">Where archives are stored on this server</div>
<div class="small text-muted">
Local backups live at <code>storage/app/backups/</code>. They are <span class="fw-bold">excluded from your code repository</span> by <code>.gitignore</code>. To migrate archives to a new server, copy this directory (and the Google Drive token from Global Settings → Backup).
</div>
</div>
</div>
</div> {{-- closes p-4 p-lg-5 --}}
</div> {{-- closes modal-body --}}
</div> {{-- closes modal-content --}}
</div> {{-- closes modal-dialog --}}
</div> {{-- closes modal --}}
@push('scripts')
<script>
$(document).ready(function () {
function renderBackups(backups) {
const $tbody = $('#backup-list-table tbody');
$tbody.empty();
if (!backups || backups.length === 0) {
$tbody.append('<tr><td colspan="4" class="text-center py-5 text-muted small fw-bold">{{ __("No system archives found.") }}</td></tr>');
return;
}
backups.forEach(function (item) {
let storageIcon = 'bi-hdd text-secondary';
let storageClass = 'bg-secondary-subtle text-secondary';
if (item.storage === 'gdrive') {
storageIcon = 'bi-google text-primary';
storageClass = 'bg-primary-subtle text-primary';
} else if (item.storage === 's3') {
storageIcon = 'bi-clouds text-warning';
storageClass = 'bg-warning-subtle text-warning-emphasis';
}
$tbody.append(`
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="avatar avatar-35 bg-light rounded-circle d-flex align-items-center justify-content-center me-3 border">
<i class="bi ${storageIcon}"></i>
</div>
<div>
<div class="fw-bold text-dark" style="font-size: 0.85rem; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${item.name}">${item.name.split('/').pop()}</div>
<div class="d-flex align-items-center gap-2">
<span class="text-muted" style="font-size: 10px;">${item.size}</span>
<span class="badge ${storageClass} border-0 rounded-pill text-uppercase" style="font-size: 8px; padding: 2px 6px;">${item.storage}</span>
</div>
</div>
</div>
</td>
<td>
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-pill px-3 py-1" style="font-size: 10px;">
${item.status}
</span>
</td>
<td>
<span class="text-muted small">${item.date}</span>
</td>
<td class="text-end pe-4">
<div class="d-flex justify-content-end gap-1">
<a href="{{ route('backup-restore.download') }}?disk=${item.storage}&path=${item.name}" class="btn btn-icon btn-sm btn-light rounded-circle" title="Download">
<i class="bi bi-download"></i>
</a>
@can('manage backup and storage')
<button type="button" class="btn btn-icon btn-sm btn-outline-primary rounded-circle btn-restore-backup" data-disk="${item.storage}" data-path="${item.name}" title="Restore">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<button type="button" class="btn btn-icon btn-sm btn-outline-danger rounded-circle btn-delete-backup" data-disk="${item.storage}" data-path="${item.name}" title="Delete">
<i class="bi bi-trash"></i>
</button>
@endcan
</div>
</td>
</tr>
`);
});
}
function loadBackups(driver = null) {
const $tbody = $('#backup-list-table tbody');
$tbody.html('<tr id="backup-loading"><td colspan="4" class="text-center py-5"><div class="spinner-border spinner-border-sm text-primary me-2"></div><span class="small fw-bold">{{ __("Refreshing inventory...") }}</span></td></tr>');
const url = new URL("{{ route('backup-restore.index') }}", window.location.origin);
if (driver) url.searchParams.append('driver', driver);
$.get(url.toString(), function (response) {
if (response.success) {
renderBackups(response.backups);
if (response.stats) {
const stats = response.stats;
$('#storage-health-widget').fadeIn();
$('#health-label').text(stats.label + " Usage");
$('#health-used').text(stats.used);
$('#health-total').text(stats.total);
$('#health-progress-bar').css('width', stats.percentage + '%');
$('#health-progress-bar').removeClass('bg-success bg-warning bg-danger').addClass('bg-' + stats.health);
$('#health-status').text(stats.percentage + '% Cap').removeClass('text-success text-warning text-danger bg-success-subtle bg-warning-subtle bg-danger-subtle').addClass('text-' + stats.health + ' bg-' + stats.health + '-subtle');
if (stats.health === 'danger') {
$('#storage-health-widget').addClass('pulse-danger border border-danger border-opacity-50');
} else {
$('#storage-health-widget').removeClass('pulse-danger border border-danger border-opacity-50');
}
$('#health-total-label').show();
$('#health-progress-wrapper').show();
if (stats.requirements && !stats.requirements.status) {
$('#requirements-alert').fadeIn();
$('#req-msg').text(stats.requirements.message);
} else {
$('#requirements-alert').hide();
}
}
} else {
$tbody.html(`<tr><td colspan="4" class="text-center py-5 text-danger small fw-bold">${response.message || '{{ __("Failed to load inventory.") }}'}</td></tr>`);
}
});
}
loadBackups($('#backup_db_driver').val());
$('#btn-refresh-backups').on('click', function () {
const $icon = $(this).find('i');
$icon.addClass('animate-spin');
loadBackups($('#backup_db_driver').val());
setTimeout(() => $icon.removeClass('animate-spin'), 1000);
});
$('#backup_db_driver').on('change', function () {
const driver = $(this).val();
// UI Toggle
if (driver === 'gdrive') {
$('#cloud_config_wrapper').slideDown();
$('#gdrive_fields').fadeIn();
} else {
$('#cloud_config_wrapper').slideUp();
}
// Dynamic Usage Update
loadBackups(driver);
});
$('#btn-create-backup').on('click', function () {
const $btn = $(this);
const original = $btn.html();
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span> Preparing...');
$.post("{{ route('backup-restore.create') }}", { _token: "{{ csrf_token() }}" }, function (response) {
if (response.success) {
renderBackups(response.backups || []);
StandardSwal.fire({ title: "Backup Started", text: response.message, icon: 'success', timer: 2000, showConfirmButton: false });
} else {
StandardSwal.fire({ title: 'Error', text: response.message, icon: 'error' });
}
}).always(() => $btn.prop('disabled', false).html(original));
});
$(document).on('click', '.btn-delete-backup', function () {
const $btn = $(this);
StandardSwal.fire({
title: "Delete Archive?",
text: "This file will be permanently removed from storage.",
icon: 'warning',
showCancelButton: true,
confirmButtonText: "Delete Now"
}).then((result) => {
if (result.isConfirmed) {
$.post("{{ route('backup-restore.delete') }}", {
_token: "{{ csrf_token() }}",
disk: $btn.data('disk'),
path: $btn.data('path')
}, function (response) {
if (response.success) {
renderBackups(response.backups);
StandardSwal.fire({ title: "Deleted", text: response.message, icon: 'success', timer: 1500, showConfirmButton: false });
}
});
}
});
});
$(document).on('click', '.btn-restore-backup', function () {
const $btn = $(this);
StandardSwal.fire({
title: "{{ __('Restore System?') }}",
html: `
<div class="text-start small">
<p class="mb-3">{{ __('This will replace your current database with the selected archive. The system will enter Maintenance Mode during the process.') }}</p>
<div class="alert alert-danger border-0 shadow-sm d-flex align-items-center gap-3 mb-0">
<i class="bi bi-exclamation-octagon display-6"></i>
<div>
<strong class="d-block">{{ __('CRITICAL ACTION') }}</strong>
{{ __('All current unsaved data will be lost permanently.') }}
</div>
</div>
</div>
`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: "{{ __('I Understand, Restore Now') }}",
confirmButtonColor: '#dc3545',
cancelButtonText: "{{ __('Cancel') }}"
}).then((result) => {
if (result.isConfirmed) {
StandardSwal.fire({
title: "{{ __('System Restoration In Progress') }}",
html: "{{ __('Please wait, do not close this window. The system is currently in Maintenance Mode.') }}",
allowOutsideClick: false,
didOpen: () => StandardSwal.showLoading()
});
$.post("{{ route('backup-restore.restore') }}", {
_token: "{{ csrf_token() }}",
disk: $btn.data('disk'),
path: $btn.data('path')
}, function (response) {
StandardSwal.fire({ title: "{{ __('Restored Successfully') }}", text: response.message, icon: 'success', timer: 3000 });
setTimeout(() => location.reload(), 3000);
}).fail(function(xhr) {
const msg = xhr.responseJSON?.message || '{{ __("Restoration failed. System is back online.") }}';
StandardSwal.fire({ title: "{{ __('Restoration Failed') }}", text: msg, icon: 'error' });
});
}
});
});
$('.btn-test-connection').on('click', function () {
const $btn = $(this);
const original = $btn.html();
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span>');
$.post("{{ route('backup-restore.test-connection') }}", {
_token: "{{ csrf_token() }}",
driver: $btn.data('driver')
}, function (response) {
StandardSwal.fire({ title: response.success ? "Success" : "Failed", text: response.message, icon: response.success ? 'success' : 'error' });
}).always(() => $btn.prop('disabled', false).html(original));
});
$('#btn-gdrive-auth').on('click', function (e) {
const clientId = $('input[name="gdrive_client_id"]').val();
const clientSecret = $('input[name="gdrive_client_secret"]').val();
if (!clientId || !clientSecret) {
e.preventDefault();
StandardSwal.fire({ title: "Auth Failed", text: "Save Client ID & Secret first.", icon: 'warning' });
}
});
});
</script>
@endpush
</x-app-layout>