feat: ✨ initial commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Backups - OPNsense Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-neutral-100 mb-2">All Backups</h1>
|
||||
<p class="text-neutral-400">View and download your backup files</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-neutral-700">
|
||||
<h2 class="text-xl font-bold text-neutral-100">Backup Files</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-neutral-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Instance</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Filename</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Size</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Upload Date</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-700">
|
||||
{% if backups %}
|
||||
{% for backup in backups %}
|
||||
<tr class="hover:bg-neutral-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-neutral-100">{{ backup.instance_name or backup.instance_identifier }}</div>
|
||||
<code class="text-xs text-neutral-400">{{ backup.instance_identifier }}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<code class="text-sm text-neutral-300 break-all">{{ backup.filename }}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-neutral-400">{{ "%.2f"|format(backup.file_size / 1024 / 1024) }} MB</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-neutral-400">{{ backup.uploaded_at.strftime('%Y-%m-%d %H:%M') if backup.uploaded_at else 'N/A' }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<a href="{{ url_for('download_backup', backup_id=backup.id) }}" class="text-orange-500 hover:text-orange-400">
|
||||
Download
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-8 text-center text-neutral-400">
|
||||
No backups found. Configure your OPNsense instances to start receiving backups.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}OPNsense Backup Manager{% endblock %}</title>
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='opnsense.png') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}">
|
||||
</head>
|
||||
<body class="bg-neutral-900 text-neutral-100 min-h-screen">
|
||||
<nav class="bg-neutral-800 border-b border-neutral-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="{{ url_for('dashboard') }}" class="flex items-center space-x-4">
|
||||
<img src="{{ url_for('static', filename='opnsense.png') }}" alt="OPNsense" class="h-8 w-8">
|
||||
<span class="text-neutral-400">Backup Manager</span>
|
||||
</a>
|
||||
</div>
|
||||
{% if session.user_id %}
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{{ url_for('dashboard') }}" class="text-neutral-300 hover:text-orange-500 px-3 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ url_for('instances') }}" class="text-neutral-300 hover:text-orange-500 px-3 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
Instances
|
||||
</a>
|
||||
<a href="{{ url_for('backups') }}" class="text-neutral-300 hover:text-orange-500 px-3 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
Backups
|
||||
</a>
|
||||
<span class="text-neutral-400">|</span>
|
||||
<span class="text-neutral-300">{{ session.username }}</span>
|
||||
<a href="{{ url_for('logout') }}" class="bg-neutral-700 hover:bg-neutral-600 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="mb-6 space-y-2">
|
||||
{% for category, message in messages %}
|
||||
<div class="{% if category == 'error' %}bg-red-900 border-red-700{% elif category == 'success' %}bg-green-900 border-green-700{% else %}bg-blue-900 border-blue-700{% endif %} border rounded-md p-4">
|
||||
<p class="text-sm">{{ message }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="bg-neutral-800 border-t border-neutral-700 mt-auto">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<p class="text-center text-neutral-400 text-sm">
|
||||
OPNsense Backup Manager
|
||||
{% if version %}
|
||||
<span class="text-neutral-500">v{{ version }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - OPNsense Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-neutral-100 mb-2">Dashboard</h1>
|
||||
<p class="text-neutral-400">Overview of your OPNsense backup system</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-neutral-800 rounded-lg p-6 border border-neutral-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-neutral-400 text-sm font-medium">Total Instances</p>
|
||||
<p class="text-3xl font-bold text-orange-500 mt-2">{{ instances|length }}</p>
|
||||
</div>
|
||||
<div class="p-3 bg-orange-500/20 rounded-lg">
|
||||
<svg class="w-8 h-8 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 rounded-lg p-6 border border-neutral-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-neutral-400 text-sm font-medium">Total Backups</p>
|
||||
<p class="text-3xl font-bold text-orange-500 mt-2">{{ backups|length }}</p>
|
||||
</div>
|
||||
<div class="p-3 bg-orange-500/20 rounded-lg">
|
||||
<svg class="w-8 h-8 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 rounded-lg p-6 border border-neutral-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-neutral-400 text-sm font-medium">SFTP Port</p>
|
||||
<p class="text-3xl font-bold text-orange-500 mt-2">{{ sftp_server.port if sftp_server else 'N/A' }}</p>
|
||||
</div>
|
||||
<div class="p-3 bg-orange-500/20 rounded-lg">
|
||||
<svg class="w-8 h-8 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold text-neutral-100">Quick Actions</h2>
|
||||
<a href="{{ url_for('new_instance') }}" class="bg-orange-500 hover:bg-orange-600 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200">
|
||||
+ Add Instance
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Instances List -->
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-neutral-700">
|
||||
<h2 class="text-xl font-bold text-neutral-100">OPNsense Instances</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-neutral-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Name</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Identifier</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Last Backup</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-700">
|
||||
{% if instances %}
|
||||
{% for instance in instances %}
|
||||
<tr class="hover:bg-neutral-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-neutral-100">{{ instance.name }}</div>
|
||||
{% if instance.description %}
|
||||
<div class="text-sm text-neutral-400">{{ instance.description }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<code class="text-sm text-orange-400">{{ instance.identifier }}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-neutral-400">
|
||||
{% if instance.last_backup %}
|
||||
{{ instance.last_backup.strftime('%Y-%m-%d %H:%M') }}
|
||||
{% else %}
|
||||
<span class="text-neutral-500">Never</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<a href="{{ url_for('instance_detail', instance_id=instance.id) }}" class="text-orange-500 hover:text-orange-400 mr-4">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-8 text-center text-neutral-400">
|
||||
No instances configured. <a href="{{ url_for('new_instance') }}" class="text-orange-500 hover:text-orange-400">Create one</a> to get started.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Backups -->
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-neutral-700 flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold text-neutral-100">Recent Backups</h2>
|
||||
<a href="{{ url_for('backups') }}" class="text-sm text-orange-500 hover:text-orange-400">View All</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-neutral-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Instance</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Filename</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Size</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Date</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-700">
|
||||
{% if backups %}
|
||||
{% for backup in backups[:10] %}
|
||||
<tr class="hover:bg-neutral-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-neutral-100">{{ backup.instance_name or backup.instance_identifier }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<code class="text-sm text-neutral-300">{{ backup.filename }}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-neutral-400">{{ "%.2f"|format(backup.file_size / 1024 / 1024) }} MB</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-neutral-400">{{ backup.uploaded_at.strftime('%Y-%m-%d %H:%M') if backup.uploaded_at else 'N/A' }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<a href="{{ url_for('download_backup', backup_id=backup.id) }}" class="text-orange-500 hover:text-orange-400">Download</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-8 text-center text-neutral-400">
|
||||
No backups yet. Configure an instance and start backing up!
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ instance.name }} - OPNsense Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-neutral-100 mb-2">{{ instance.name }}</h1>
|
||||
<p class="text-neutral-400">{{ instance.description or 'OPNsense backup instance' }}</p>
|
||||
</div>
|
||||
<a href="{{ url_for('instances') }}" class="bg-neutral-700 hover:bg-neutral-600 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200">
|
||||
← Back to Instances
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- SFTP URI for OPNsense -->
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||
<h2 class="text-xl font-bold text-neutral-100 mb-4">SFTP Target Location (URI)</h2>
|
||||
<p class="text-sm text-neutral-400 mb-4">Use this URI format in OPNsense backup configuration:</p>
|
||||
<div class="bg-neutral-900 rounded-md p-4 border border-neutral-600 mb-4">
|
||||
<code class="text-sm text-orange-400 break-all block">{{ sftp_uri }}</code>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
onclick="copyToClipboard('{{ sftp_uri }}')"
|
||||
class="bg-orange-500 hover:bg-orange-600 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200 text-sm"
|
||||
>
|
||||
Copy URI
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-4 bg-neutral-700 rounded-md p-3">
|
||||
<p class="text-xs text-neutral-400">
|
||||
<strong>Note:</strong> The double slash (<code>//</code>) or single slash after the host indicates the root path.
|
||||
Backups will be stored automatically in the instance-specific directory.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSH Private Key -->
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||
<h2 class="text-xl font-bold text-neutral-100 mb-4">SSH Private Key</h2>
|
||||
<p class="text-sm text-neutral-400 mb-4">
|
||||
<strong class="text-orange-400">IMPORTANT:</strong> OPNsense requires the <strong>private key</strong> for authentication.
|
||||
Download and paste this into OPNsense's SSH key field.
|
||||
</p>
|
||||
{% if private_key_content %}
|
||||
<div class="bg-neutral-900 rounded-md p-4 border border-neutral-600 mb-4">
|
||||
<code class="text-sm text-neutral-300 whitespace-pre-wrap break-all block">{{ private_key_content }}</code>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
onclick="copyToClipboard('{{ private_key_content }}')"
|
||||
class="bg-orange-500 hover:bg-orange-600 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200 text-sm"
|
||||
>
|
||||
Copy Private Key
|
||||
</button>
|
||||
<a
|
||||
href="{{ url_for('download_private_key', instance_id=instance.id) }}"
|
||||
class="bg-neutral-700 hover:bg-neutral-600 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200 text-sm"
|
||||
>
|
||||
Download Private Key
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-red-900/30 border border-red-700 rounded-md p-4">
|
||||
<p class="text-sm text-red-400">Private key file not found on disk.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-4 bg-yellow-900/30 border border-yellow-700 rounded-md p-3">
|
||||
<p class="text-xs text-yellow-400">
|
||||
<strong>Security Warning:</strong> Keep this private key secure. Anyone with access to it can authenticate as this instance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OPNsense Configuration Steps -->
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||
<h2 class="text-xl font-bold text-neutral-100 mb-4">OPNsense Configuration Steps</h2>
|
||||
<ol class="list-decimal list-inside space-y-3 text-neutral-300">
|
||||
<li>Navigate to <strong class="text-neutral-100">System → Configuration → Backups</strong> in OPNsense</li>
|
||||
<li>Click on <strong class="text-neutral-100">Add Backup Target</strong> or edit existing</li>
|
||||
<li>Select <strong class="text-neutral-100">SFTP</strong> as the backup type</li>
|
||||
<li>Enter the following:
|
||||
<ul class="list-disc list-inside ml-6 mt-2 space-y-2 text-neutral-400">
|
||||
<li><strong class="text-neutral-300">Target location (URI):</strong>
|
||||
<code class="text-orange-400 bg-neutral-900 px-2 py-1 rounded">{{ sftp_uri }}</code>
|
||||
<br><span class="text-xs">Copy the URI from above</span>
|
||||
</li>
|
||||
<li><strong class="text-neutral-300">SSH Private Key:</strong>
|
||||
Copy or download the private key from above
|
||||
<br><span class="text-xs text-yellow-400">⚠️ OPNsense requires the private key, not the public key!</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Configure your backup schedule and encryption options as desired</li>
|
||||
<li>Save and test the backup configuration</li>
|
||||
</ol>
|
||||
<div class="mt-4 bg-blue-900/30 border border-blue-700 rounded-md p-3">
|
||||
<p class="text-xs text-blue-400">
|
||||
<strong>Tip:</strong> After saving, test the connection to verify authentication works correctly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backups List -->
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-neutral-700">
|
||||
<h2 class="text-xl font-bold text-neutral-100">Backups</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-neutral-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Filename</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Size</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Upload Date</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-700">
|
||||
{% if backups %}
|
||||
{% for backup in backups %}
|
||||
<tr class="hover:bg-neutral-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<code class="text-sm text-neutral-300">{{ backup.filename }}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-neutral-400">{{ "%.2f"|format(backup.file_size / 1024 / 1024) }} MB</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-neutral-400">{{ backup.uploaded_at.strftime('%Y-%m-%d %H:%M') if backup.uploaded_at else 'N/A' }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<a href="{{ url_for('download_backup', backup_id=backup.id) }}" class="text-orange-500 hover:text-orange-400">Download</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-8 text-center text-neutral-400">
|
||||
No backups yet. Configure OPNsense to start backing up to this instance.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
alert('Copied to clipboard!');
|
||||
}, function(err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
alert('Failed to copy to clipboard');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Instances - OPNsense Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-neutral-100 mb-2">OPNsense Instances</h1>
|
||||
<p class="text-neutral-400">Manage your OPNsense backup instances</p>
|
||||
</div>
|
||||
<a href="{{ url_for('new_instance') }}" class="bg-orange-500 hover:bg-orange-600 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200">
|
||||
+ Add Instance
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-neutral-700">
|
||||
<h2 class="text-xl font-bold text-neutral-100">Configured Instances</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-neutral-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Name</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Identifier</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Last Backup</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-700">
|
||||
{% if instances %}
|
||||
{% for instance in instances %}
|
||||
<tr class="hover:bg-neutral-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-neutral-100">{{ instance.name }}</div>
|
||||
{% if instance.description %}
|
||||
<div class="text-sm text-neutral-400">{{ instance.description }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<code class="text-sm text-orange-400">{{ instance.identifier }}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-neutral-400">
|
||||
{% if instance.last_backup %}
|
||||
{{ instance.last_backup.strftime('%Y-%m-%d %H:%M') }}
|
||||
{% else %}
|
||||
<span class="text-neutral-500">Never</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<a href="{{ url_for('instance_detail', instance_id=instance.id) }}" class="text-orange-500 hover:text-orange-400">View Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-8 text-center text-neutral-400">
|
||||
No instances configured. <a href="{{ url_for('new_instance') }}" class="text-orange-500 hover:text-orange-400">Create one</a> to get started.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login - OPNsense Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[calc(100vh-12rem)] flex items-center justify-center">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-neutral-800 rounded-lg shadow-lg p-8 border border-neutral-700">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-orange-500 mb-2">OPNsense Backup Manager</h1>
|
||||
<p class="text-neutral-400">Sign in to manage your backups</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('login') }}" class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-neutral-300 mb-2">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
required
|
||||
class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
placeholder="Enter your username"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-neutral-300 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
placeholder="Enter your password"
|
||||
>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 p-4 bg-neutral-700 rounded-md">
|
||||
<p class="text-xs text-neutral-400">
|
||||
Default credentials: <code class="text-orange-400">admin / admin</code>
|
||||
<br>Please change these after first login!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}New Instance - OPNsense Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-neutral-100 mb-2">Add New OPNsense Instance</h1>
|
||||
<p class="text-neutral-400">Create a new instance to start receiving backups</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||
<form method="POST" action="{{ url_for('new_instance') }}" class="space-y-6">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-neutral-300 mb-2">
|
||||
Instance Name <span class="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
placeholder="e.g., Main Router, Office Firewall"
|
||||
>
|
||||
<p class="mt-1 text-sm text-neutral-400">A friendly name for this instance</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="identifier" class="block text-sm font-medium text-neutral-300 mb-2">
|
||||
Identifier <span class="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="identifier"
|
||||
name="identifier"
|
||||
required
|
||||
pattern="[a-z0-9-_]+"
|
||||
class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
placeholder="e.g., main-router, office-fw"
|
||||
>
|
||||
<p class="mt-1 text-sm text-neutral-400">Unique identifier (lowercase, alphanumeric, dashes, underscores only). This will be used as the SFTP username.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-neutral-300 mb-2">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
rows="3"
|
||||
class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
placeholder="Optional description for this instance"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-4 pt-4">
|
||||
<a href="{{ url_for('instances') }}" class="px-4 py-2 bg-neutral-700 hover:bg-neutral-600 text-white font-medium rounded-md transition-colors duration-200">
|
||||
Cancel
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white font-medium rounded-md transition-colors duration-200"
|
||||
>
|
||||
Create Instance
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||
<h3 class="text-lg font-semibold text-neutral-100 mb-4">What happens next?</h3>
|
||||
<ol class="list-decimal list-inside space-y-2 text-neutral-300">
|
||||
<li>An SSH key pair will be automatically generated for this instance</li>
|
||||
<li>You'll be provided with the public key to add to your OPNsense configuration</li>
|
||||
<li>Use the instance identifier as the SFTP username when configuring OPNsense</li>
|
||||
<li>Backups will be automatically stored and tracked</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user