Files

236 lines
13 KiB
HTML

{% extends "layout.html" %}
{% block title %}Settings - BZOD{% endblock %}
{% block active_settings %}active{% endblock %}
{% block header_title %}System Settings{% endblock %}
{% block content %}
{% if let Some(msg) = success %}
<div class="alert alert-success">
{{ msg }}
</div>
{% endif %}
{% if let Some(err) = error %}
<div class="alert alert-error">
{{ err }}
</div>
{% endif %}
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; align-items: start;">
<!-- Left Column -->
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
<!-- Change Password -->
<div class="card">
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Change Password
</h3>
<form action="/admin/settings/password" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" class="form-input" required autocomplete="current-password">
</div>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" class="form-input" required minlength="8" autocomplete="new-password">
</div>
<button type="submit" class="btn" style="width: 100%; margin-top: 0.5rem;">Update Password</button>
</form>
</div>
<!-- Data Retention -->
<div class="card">
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Analytics Retention Policy
</h3>
<form action="/admin/settings/retention" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-group">
<label for="retention">Visits Logs Retention</label>
<select id="retention" name="retention">
<option value="30" {% if data_retention == "30" %}selected{% endif %}>30 Days</option>
<option value="90" {% if data_retention == "90" %}selected{% endif %}>90 Days</option>
<option value="365" {% if data_retention == "365" %}selected{% endif %}>365 Days</option>
<option value="unlimited" {% if data_retention == "unlimited" %}selected{% endif %}>Unlimited</option>
</select>
</div>
<p style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.4;">
Old visits logs are cleaned up daily. Changing this policy does not affect aggregated monthly/yearly summaries.
</p>
<button type="submit" class="btn" style="width: 100%;">Save Retention Policy</button>
</form>
</div>
<!-- Maintenance Operations -->
<div class="card">
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
Maintenance & DB Utilities
</h3>
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div>
<form action="/admin/settings/compact" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-secondary" style="width: 100%; text-align: left; justify-content: flex-start;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
Compact Databases (VACUUM)
</button>
</form>
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem; margin-left: 0.5rem;">
Reclaims unused space and defragments all SQLite database files.
</p>
</div>
<div>
<a href="/admin/settings/backup" class="btn" style="width: 100%; text-align: left; justify-content: flex-start; background: #047857;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Download Backup (.tar.gz)
</a>
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem; margin-left: 0.5rem;">
Generates a tarball of admin.db, content.db, and analytics.db.
</p>
</div>
<div style="border-top: 1px solid var(--border-color); padding-top: 1rem; margin-top: 0.5rem;">
<form action="/admin/settings/restore" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label for="backup_file" style="display: block; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.5rem;">Restore Database from Backup</label>
<input type="file" id="backup_file" name="backup_file" class="form-input" style="padding: 0.35rem 0.5rem; margin-bottom: 0.75rem;" required accept=".tar.gz">
<p style="font-size: 0.8rem; color: #fca5a5; margin-bottom: 0.75rem; line-height: 1.4; font-weight: 500;">
Warning: This operation will overwrite all current data.
</p>
<div class="form-group" style="margin-bottom: 0.75rem;">
<label for="confirm_text" style="font-size: 0.75rem; color: var(--text-secondary);">Type RESTORE to continue:</label>
<input type="text" id="confirm_text" name="confirm_text" class="form-input" placeholder="RESTORE" required autocomplete="off">
</div>
<button type="submit" class="btn btn-danger" style="width: 100%; justify-content: center;">
Restore Backup
</button>
</form>
</div>
</div>
</div>
<!-- Bulk QR Code Export -->
<div class="card">
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="3"></rect><rect x="14" y="7" width="3" height="3"></rect><rect x="7" y="14" width="3" height="3"></rect><rect x="14" y="14" width="3" height="3"></rect></svg>
Bulk QR Code Export
</h3>
<form action="/admin/settings/bulk-qr" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-group">
<label for="format">Export Format</label>
<select id="format" name="format">
<option value="png" selected>PNG Images (256x256)</option>
<option value="svg">SVG Vector Graphics</option>
</select>
</div>
<p style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.4;">
Generates a ZIP archive containing QR codes for all shortened links in your registry, alongside a <code>manifest.csv</code> file mapping codes to their destinations.
</p>
<button type="submit" class="btn" style="width: 100%; background: var(--primary-grad);">Download ZIP Archive</button>
</form>
</div>
</div>
<!-- Right Column: API Keys -->
<div class="card" style="padding: 0; overflow: hidden;">
<div style="padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border-color);">
<h3 style="font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
REST API Tokens
</h3>
</div>
<div style="padding: 1.5rem; border-bottom: 1px solid var(--border-color);">
<form action="/admin/settings/api-keys/create" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-group">
<label for="key_name">Generate Token Name</label>
<div style="display: flex; gap: 0.5rem;">
<input type="text" id="key_name" name="key_name" class="form-input" placeholder="e.g. HomeAssistant Script" required>
<button type="submit" class="btn">Generate</button>
</div>
</div>
</form>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Key Description</th>
<th>Created</th>
<th>Last Used</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% if api_keys.is_empty() %}
<tr>
<td colspan="4" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
No active API tokens generated yet.
</td>
</tr>
{% else %}
{% for key in api_keys %}
<tr>
<td style="font-weight: 600; color: var(--text-primary);">
{{ key.name }}
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
Key Prefix: {{ key.key_hash[0..8] }}...
</div>
</td>
<td style="font-size: 0.8rem; color: var(--text-secondary);">
{{ key.created_at[0..10] }}
</td>
<td style="font-size: 0.8rem; color: var(--text-secondary);">
{% if let Some(last) = key.last_used_at %}
{{ last[0..16].replace("T", " ") }}
{% else %}
Never
{% endif %}
</td>
<td>
<form action="/admin/settings/api-keys/revoke/{{ key.id }}" method="POST" onsubmit="return confirm('Are you sure you want to revoke this API token? Any integrations using it will fail.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-danger" style="padding: 0.4rem 0.6rem; font-size: 0.8rem;">
Revoke
</button>
</form>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}