fix: merge main branch and renumber migration 027→037
Resolve 3 merge conflicts and renumber the automation_hooks migration to follow main's migration chain (036_add_document_translation_fields). Conflicts resolved: - app/api/__init__.py: add automation_router alongside main's new routers - app/utils/settings_service.py: add automation_hooks_enabled alongside compliance_enabled - tests/conftest.py: add AutomationHook alongside AuditLog/ComplianceTemplate imports Migration renumbered: - 027_add_automation_hooks → 037_add_automation_hooks - down_revision: 026_add_scheduled_jobs → 036_add_document_translation_fields Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Backup Management{% endblock %}
|
||||
{% block title %}{{ _("backup.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8" x-data="backupDashboard()">
|
||||
@@ -9,10 +9,10 @@
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
<i class="fas fa-database mr-2 text-blue-600" aria-hidden="true"></i>
|
||||
Backup Management
|
||||
{{ _("backup.heading") }}
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).
|
||||
{{ _("backup.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
@@ -21,26 +21,26 @@
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-clock mr-1 text-blue-500" aria-hidden="true"></i> Backup Now (Hourly)
|
||||
<i class="fas fa-clock mr-1 text-blue-500" aria-hidden="true"></i> {{ _("backup.trigger_hourly") }}
|
||||
</button>
|
||||
<button @click="triggerBackup('daily')"
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-calendar-day mr-1 text-green-500" aria-hidden="true"></i> Backup Now (Daily)
|
||||
<i class="fas fa-calendar-day mr-1 text-green-500" aria-hidden="true"></i> {{ _("backup.trigger_daily") }}
|
||||
</button>
|
||||
<button @click="triggerBackup('weekly')"
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-calendar-week mr-1 text-purple-500" aria-hidden="true"></i> Backup Now (Weekly)
|
||||
<i class="fas fa-calendar-week mr-1 text-purple-500" aria-hidden="true"></i> {{ _("backup.trigger_weekly") }}
|
||||
</button>
|
||||
<button @click="runCleanup()"
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 min-h-[44px]"
|
||||
title="Run retention cleanup now">
|
||||
<i class="fas fa-broom mr-1 text-orange-400" aria-hidden="true"></i> Clean Up
|
||||
title="{{ _('backup.cleanup_title') }}">
|
||||
<i class="fas fa-broom mr-1 text-orange-400" aria-hidden="true"></i> {{ _("backup.clean_up") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,18 +60,18 @@
|
||||
role="dialog" aria-modal="true" :aria-labelledby="'confirmTitle'">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-sm w-full p-6"
|
||||
@keydown.escape.window="confirmOpen = false">
|
||||
<h2 id="confirmTitle" class="text-lg font-semibold text-gray-900 mb-2">Confirm action</h2>
|
||||
<h2 id="confirmTitle" class="text-lg font-semibold text-gray-900 mb-2">{{ _("backup.confirm_title") }}</h2>
|
||||
<p class="text-sm text-gray-600 mb-4" x-text="confirmMsg"></p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button @click="confirmOpen = false; confirmResolve(false)"
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 min-h-[44px]">
|
||||
Cancel
|
||||
{{ _("common.cancel") }}
|
||||
</button>
|
||||
<button @click="confirmOpen = false; confirmResolve(true)"
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 min-h-[44px]">
|
||||
Confirm
|
||||
{{ _("backup.confirm_btn") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,36 +81,36 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<!-- Backup enabled -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Auto-backup</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_auto_backup") }}</p>
|
||||
<p class="mt-1 text-lg font-semibold {% if backup_enabled %}text-green-600{% else %}text-red-600{% endif %}">
|
||||
{% if backup_enabled %}<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> Enabled
|
||||
{% else %}<i class="fas fa-times-circle mr-1" aria-hidden="true"></i> Disabled{% endif %}
|
||||
{% if backup_enabled %}<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> {{ _("common.enabled") }}
|
||||
{% else %}<i class="fas fa-times-circle mr-1" aria-hidden="true"></i> {{ _("common.disabled") }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Remote destination -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Remote destination</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_remote_dest") }}</p>
|
||||
<p class="mt-1 text-lg font-semibold text-gray-800">
|
||||
{% if backup_remote_destination %}
|
||||
<i class="fas fa-cloud-upload-alt mr-1 text-blue-500" aria-hidden="true"></i>
|
||||
{{ backup_remote_destination }}
|
||||
{% else %}
|
||||
<span class="text-gray-400"><i class="fas fa-hdd mr-1" aria-hidden="true"></i> Local only</span>
|
||||
<span class="text-gray-400"><i class="fas fa-hdd mr-1" aria-hidden="true"></i> {{ _("backup.local_only") }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Retention hourly -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Retention</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_retention") }}</p>
|
||||
<p class="mt-1 text-sm text-gray-700">
|
||||
<span class="font-semibold">{{ backup_retain_hourly }}</span> hourly ·
|
||||
<span class="font-semibold">{{ backup_retain_daily }}</span> daily ·
|
||||
<span class="font-semibold">{{ backup_retain_weekly }}</span> weekly
|
||||
<span class="font-semibold">{{ backup_retain_hourly }}</span> {{ _("backup.type_hourly") }} ·
|
||||
<span class="font-semibold">{{ backup_retain_daily }}</span> {{ _("backup.type_daily") }} ·
|
||||
<span class="font-semibold">{{ backup_retain_weekly }}</span> {{ _("backup.type_weekly") }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Total backup size -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Total local size</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_total_size") }}</p>
|
||||
<p class="mt-1 text-lg font-semibold text-gray-800" id="totalSize">
|
||||
{{ (total_size / 1048576) | round(2) }} MB
|
||||
</p>
|
||||
@@ -120,17 +120,17 @@
|
||||
<!-- Upload restore section -->
|
||||
<div class="bg-white shadow rounded-lg p-6 mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-1">
|
||||
<i class="fas fa-upload mr-2 text-yellow-500" aria-hidden="true"></i>Restore from File
|
||||
<i class="fas fa-upload mr-2 text-yellow-500" aria-hidden="true"></i>{{ _("backup.restore_heading") }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Upload a <code>.db.gz</code> backup archive to restore the database.
|
||||
<strong class="text-red-600">This will overwrite all current data.</strong>
|
||||
{{ _("backup.restore_desc_pre") }} <code>.db.gz</code> {{ _("backup.restore_desc_mid") }}
|
||||
<strong class="text-red-600">{{ _("backup.restore_desc_warning") }}</strong>
|
||||
</p>
|
||||
<form id="restoreForm" @submit.prevent="submitRestore" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="flex flex-col sm:flex-row gap-3 items-start sm:items-end">
|
||||
<div>
|
||||
<label for="restoreFile" class="block text-sm font-medium text-gray-700 mb-1">Backup archive (.db.gz)</label>
|
||||
<label for="restoreFile" class="block text-sm font-medium text-gray-700 mb-1">{{ _("backup.restore_file_label") }}</label>
|
||||
<input type="file" id="restoreFile" name="file" accept=".gz"
|
||||
required
|
||||
class="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||
@@ -139,7 +139,7 @@
|
||||
:disabled="restoring"
|
||||
class="inline-flex items-center px-4 py-2 bg-yellow-500 hover:bg-yellow-600 text-white text-sm font-medium rounded-md disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-undo mr-2" aria-hidden="true"></i>
|
||||
<span x-text="restoring ? 'Restoring…' : 'Restore'"></span>
|
||||
<span x-text="restoring ? '{{ _('backup.restoring') }}' : '{{ _('backup.restore_btn') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -149,30 +149,30 @@
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-list mr-2 text-gray-500" aria-hidden="true"></i>Backup Archives
|
||||
<span class="ml-2 text-sm font-normal text-gray-400">({{ records | length }} records)</span>
|
||||
<i class="fas fa-list mr-2 text-gray-500" aria-hidden="true"></i>{{ _("backup.archives_heading") }}
|
||||
<span class="ml-2 text-sm font-normal text-gray-400">({{ records | length }} {{ _("backup.records_label") }})</span>
|
||||
</h2>
|
||||
<!-- Tier filter -->
|
||||
<div class="flex gap-2 text-sm">
|
||||
<button @click="filterType = ''" :class="filterType === '' ? 'bg-gray-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">All</button>
|
||||
<button @click="filterType = 'hourly'" :class="filterType === 'hourly' ? 'bg-blue-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">Hourly</button>
|
||||
<button @click="filterType = 'daily'" :class="filterType === 'daily' ? 'bg-green-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">Daily</button>
|
||||
<button @click="filterType = 'weekly'" :class="filterType === 'weekly' ? 'bg-purple-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">Weekly</button>
|
||||
<button @click="filterType = ''" :class="filterType === '' ? 'bg-gray-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("common.all") }}</button>
|
||||
<button @click="filterType = 'hourly'" :class="filterType === 'hourly' ? 'bg-blue-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("backup.type_hourly") }}</button>
|
||||
<button @click="filterType = 'daily'" :class="filterType === 'daily' ? 'bg-green-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("backup.type_daily") }}</button>
|
||||
<button @click="filterType = 'weekly'" :class="filterType === 'weekly' ? 'bg-purple-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("backup.type_weekly") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if records %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200" aria-label="Backup archives">
|
||||
<table class="min-w-full divide-y divide-gray-200" aria-label="{{ _('backup.table_aria') }}">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Filename</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Size</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Storage</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_filename") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_type") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_created") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_size") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("common.status") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_storage") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@@ -186,15 +186,15 @@
|
||||
<td class="px-4 py-3 text-sm">
|
||||
{% if r.backup_type == 'hourly' %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<i class="fas fa-clock mr-1" aria-hidden="true"></i> hourly
|
||||
<i class="fas fa-clock mr-1" aria-hidden="true"></i> {{ _("backup.type_hourly") }}
|
||||
</span>
|
||||
{% elif r.backup_type == 'daily' %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
||||
<i class="fas fa-calendar-day mr-1" aria-hidden="true"></i> daily
|
||||
<i class="fas fa-calendar-day mr-1" aria-hidden="true"></i> {{ _("backup.type_daily") }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
|
||||
<i class="fas fa-calendar-week mr-1" aria-hidden="true"></i> weekly
|
||||
<i class="fas fa-calendar-week mr-1" aria-hidden="true"></i> {{ _("backup.type_weekly") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -209,7 +209,7 @@
|
||||
<td class="px-4 py-3 text-sm">
|
||||
{% if r.status == 'ok' %}
|
||||
<span class="inline-flex items-center text-green-700">
|
||||
<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> ok
|
||||
<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> {{ _("backup.status_ok") }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center text-red-600">
|
||||
@@ -220,7 +220,7 @@
|
||||
<td class="px-4 py-3 text-sm text-gray-600">
|
||||
{% if r.local_path %}
|
||||
<span class="inline-flex items-center" title="{{ r.local_path }}">
|
||||
<i class="fas fa-hdd mr-1 text-gray-400" aria-hidden="true"></i> local
|
||||
<i class="fas fa-hdd mr-1 text-gray-400" aria-hidden="true"></i> {{ _("backup.storage_local") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if r.remote_destination %}
|
||||
@@ -236,15 +236,15 @@
|
||||
{% if r.local_path %}
|
||||
<a href="/api/admin/backup/{{ r.id }}/download"
|
||||
class="inline-flex items-center px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-50 text-gray-700 min-h-[32px]"
|
||||
aria-label="Download backup {{ r.filename }}">
|
||||
<i class="fas fa-download mr-1" aria-hidden="true"></i> Download
|
||||
aria-label="{{ _('common.download') }} {{ r.filename }}">
|
||||
<i class="fas fa-download mr-1" aria-hidden="true"></i> {{ _("common.download") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button @click="deleteBackup({{ r.id }}, '{{ r.filename }}')"
|
||||
type="button"
|
||||
class="inline-flex items-center px-2 py-1 text-xs border border-red-300 rounded hover:bg-red-50 text-red-600 ml-1 min-h-[32px]"
|
||||
aria-label="Delete backup {{ r.filename }}">
|
||||
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> Delete
|
||||
aria-label="{{ _('common.delete') }} {{ r.filename }}">
|
||||
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> {{ _("common.delete") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -255,8 +255,8 @@
|
||||
{% else %}
|
||||
<div class="px-6 py-12 text-center text-gray-400">
|
||||
<i class="fas fa-database text-4xl mb-3" aria-hidden="true"></i>
|
||||
<p class="text-lg font-medium">No backups yet.</p>
|
||||
<p class="text-sm">Click <em>Backup Now</em> to create your first backup.</p>
|
||||
<p class="text-lg font-medium">{{ _("backup.no_backups") }}</p>
|
||||
<p class="text-sm">{{ _("backup.no_backups_hint_pre") }}<em>{{ _("backup.backup_now") }}</em>{{ _("backup.no_backups_hint_post") }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -264,16 +264,15 @@
|
||||
<!-- Retention explanation -->
|
||||
<div class="mt-8 bg-gray-50 border border-gray-200 rounded-lg p-5">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">
|
||||
<i class="fas fa-info-circle mr-1 text-blue-400" aria-hidden="true"></i> Retention policy
|
||||
<i class="fas fa-info-circle mr-1 text-blue-400" aria-hidden="true"></i> {{ _("backup.retention_heading") }}
|
||||
</h3>
|
||||
<ul class="text-sm text-gray-600 space-y-1 list-disc list-inside">
|
||||
<li><strong>Hourly</strong> – kept for 4 days ({{ backup_retain_hourly }} snapshots)</li>
|
||||
<li><strong>Daily</strong> – kept for 3 weeks ({{ backup_retain_daily }} snapshots)</li>
|
||||
<li><strong>Weekly</strong> – kept for ~3 months ({{ backup_retain_weekly }} snapshots)</li>
|
||||
<li><strong>{{ _("backup.type_hourly") }}</strong> {{ _("backup.retention_hourly_detail") }} ({{ backup_retain_hourly }} {{ _("backup.snapshots") }})</li>
|
||||
<li><strong>{{ _("backup.type_daily") }}</strong> {{ _("backup.retention_daily_detail") }} ({{ backup_retain_daily }} {{ _("backup.snapshots") }})</li>
|
||||
<li><strong>{{ _("backup.type_weekly") }}</strong> {{ _("backup.retention_weekly_detail") }} ({{ backup_retain_weekly }} {{ _("backup.snapshots") }})</li>
|
||||
</ul>
|
||||
<p class="text-xs text-gray-400 mt-2">
|
||||
Backups beyond these limits are automatically pruned after each new backup is created.
|
||||
Use the <em>Clean Up</em> button to apply retention manually.
|
||||
{{ _("backup.retention_note_pre") }}<em>{{ _("backup.clean_up") }}</em>{{ _("backup.retention_note_post") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user