Files
vibedns/web/templates/pages/settings_database.html
T
2026-08-16 21:18:45 -05:00

276 lines
12 KiB
HTML

{{define "content"}}
{{$s := .Data.S}}{{$db := .Data.DBStats}}{{$csrf := .CSRF}}{{$bs := .Data.BackupStatus}}
<h1 class="page-title mb-1">Settings</h1>
<p class="text-body-secondary mb-4">Server configuration, stored in the database.</p>
{{template "settingsnav" .}}
{{if .Data.PendingRestore}}
<div class="alert alert-danger d-flex flex-wrap align-items-center gap-3">
<i class="bi bi-exclamation-octagon fs-4"></i>
<div class="flex-grow-1">
<div class="fw-semibold">A database restore is staged</div>
<div class="small">
The next time this server starts, the live database will be replaced by the staged
backup. The current database is preserved alongside it, so this remains reversible.
</div>
</div>
{{template "confirmform" dict
"Action" "/settings/database/restore/cancel" "CSRF" $csrf
"Label" "Cancel restore" "Icon" "bi-x-lg"
"Class" "btn btn-sm btn-outline-dark"
"Message" "Discard the staged restore and keep the current database?"}}
</div>
{{end}}
<div class="row g-3">
<div class="col-12 col-xl-4">
<div class="card mb-3">
<div class="card-header">Database</div>
<div class="card-body">
<dl class="row small mb-0">
<dt class="col-5 text-body-secondary">Path</dt>
<dd class="col-7 mono text-break">{{$db.Path}}</dd>
<dt class="col-5 text-body-secondary">Size</dt>
<dd class="col-7">{{bytes $db.SizeBytes}}</dd>
<dt class="col-5 text-body-secondary">Write-ahead log</dt>
<dd class="col-7">{{bytes $db.WALBytes}}</dd>
<dt class="col-5 text-body-secondary">Schema version</dt>
<dd class="col-7">{{$db.SchemaVer}}</dd>
<dt class="col-5 text-body-secondary">Zones</dt>
<dd class="col-7">{{num $db.Zones}}</dd>
<dt class="col-5 text-body-secondary">Records</dt>
<dd class="col-7">{{num $db.Records}}</dd>
<dt class="col-5 text-body-secondary">List domains</dt>
<dd class="col-7">{{num $db.Domains}}</dd>
<dt class="col-5 text-body-secondary">Query log rows</dt>
<dd class="col-7">{{num $db.QueryLogs}}</dd>
<dt class="col-5 text-body-secondary">Audit rows</dt>
<dd class="col-7">{{num $db.AuditLogs}}</dd>
</dl>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Schema migrations</div>
<div class="card-body p-0">
<table class="table table-sm table-compact mb-0">
<tbody>
{{range .Data.Migrations}}
<tr>
<td class="mono small">{{.Version}}_{{.Name}}</td>
<td class="text-end">
{{if .Drifted}}<span class="badge text-bg-danger" title="The file changed after being applied">drift</span>
{{else if .Applied}}<span class="badge text-bg-success">applied</span>
{{else}}<span class="badge text-bg-warning">pending</span>{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">Configuration transfer</div>
<div class="card-body">
<p class="small text-body-secondary">
A versioned JSON export of zones, records, networks, policies, list definitions and
settings. Password hashes and API token secrets are deliberately excluded.
</p>
<div class="d-grid gap-2 mb-3">
<a href="/settings/config/export" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-download me-1"></i>Export configuration
</a>
<a href="/settings/config/export?include_domains=true" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-download me-1"></i>Export including list domains
</a>
</div>
<form method="post" action="/settings/config/import" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{$csrf}}">
<div class="mb-2">
<label class="form-label small" for="configFile">Import a configuration export</label>
<input type="file" class="form-control form-control-sm" id="configFile" name="file"
accept=".json,application/json" required>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="applySettings"
name="apply_settings" value="true">
<label class="form-check-label small" for="applySettings">Also apply the settings</label>
</div>
<div class="form-text mb-2">
Objects that already exist are kept as they are and reported, never overwritten.
</div>
<button type="submit" class="btn btn-sm btn-outline-primary w-100">
<i class="bi bi-upload me-1"></i>Import
</button>
</form>
</div>
</div>
</div>
<div class="col-12 col-xl-8">
<form method="post" action="/settings/database">
<input type="hidden" name="_csrf" value="{{$csrf}}">
<div class="card mb-3">
<div class="card-header">Automatic backups</div>
<div class="card-body">
<div class="form-check form-switch mb-3">
<input type="hidden" name="backup_enabled" value="false">
<input class="form-check-input" type="checkbox" role="switch" id="backupEnabled"
name="backup_enabled" value="true" {{if $s.Backup.Enabled}}checked{{end}}>
<label class="form-check-label" for="backupEnabled">Back up the database on a schedule</label>
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label" for="backupDir">Backup directory</label>
<input type="text" class="form-control mono" id="backupDir" name="backup_dir"
value="{{$s.Backup.Directory}}" placeholder="/var/lib/vibedns/backups">
</div>
<div class="col-6 col-md-3">
<label class="form-label" for="intervalHours">Interval</label>
<div class="input-group">
<input type="number" class="form-control" id="intervalHours" name="interval_hours"
value="{{$s.Backup.IntervalHours}}" min="1" max="8760">
<span class="input-group-text">h</span>
</div>
</div>
<div class="col-6 col-md-3">
<label class="form-label" for="retention">Keep</label>
<div class="input-group">
<input type="number" class="form-control" id="retention" name="retention"
value="{{$s.Backup.Retention}}" min="1" max="1000">
<span class="input-group-text">files</span>
</div>
</div>
</div>
<div class="alert alert-info small mt-3 mb-0">
<i class="bi bi-info-circle me-1"></i>
Backups use SQLite's <span class="mono">VACUUM INTO</span>, which writes a
transactionally consistent copy while the database is in use. Copying the file
with <span class="mono">cp</span> instead would capture a torn snapshot whose
committed data still lives in the write-ahead log.
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>Save backup settings
</button>
</div>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<span>
Backups
{{if $bs.Count}}
<span class="text-body-secondary small">
— {{$bs.Count}} file{{if ne $bs.Count 1}}s{{end}}, {{bytes $bs.TotalBytes}}
</span>
{{end}}
</span>
{{template "postform" dict
"Action" "/settings/database/backup" "CSRF" $csrf
"Label" "Back up now" "Icon" "bi-play-fill"
"Class" "btn btn-sm btn-primary"}}
</div>
{{if $bs.LastError}}
<div class="card-body pb-0">
<div class="alert alert-danger small mb-0">
<i class="bi bi-exclamation-triangle me-1"></i>
Last backup failed: {{$bs.LastError}}
</div>
</div>
{{end}}
{{if .Data.Backups}}
<div class="table-responsive">
<table class="table table-hover align-middle table-compact">
<thead>
<tr><th>File</th><th class="text-end">Size</th><th>Created</th><th class="row-actions">Actions</th></tr>
</thead>
<tbody>
{{range .Data.Backups}}
<tr>
<td class="mono small">{{.Name}}</td>
<td class="text-end">{{bytes .SizeBytes}}</td>
<td class="small text-body-secondary" title="{{datetime .CreatedAt}}">{{timeAgo .CreatedAt}}</td>
<td class="row-actions">
<div class="btn-group">
<a href="/settings/database/backup/{{.Name}}" class="btn btn-sm btn-outline-secondary"
title="Download this backup">
<i class="bi bi-download"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-warning"
data-bs-toggle="modal" data-bs-target="#restoreModal"
data-backup-name="{{.Name}}" title="Restore from this backup">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
{{template "confirmform" dict
"Action" (printf "/settings/database/backup/%s/delete" .Name) "CSRF" $csrf
"Icon" "bi-trash" "Title" "Delete this backup"
"Message" (printf "Delete the backup file %s?" .Name)}}
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="card-body">
{{template "empty" dict "Icon" "bi-archive" "Title" "No backups yet"
"Message" "Run one now, or enable the schedule above. Backups are written to the directory configured here and are restricted to the owning user."}}
</div>
{{end}}
</div>
</div>
</div>
{{/* ---- Restore modal ---- */}}
<div class="modal fade" id="restoreModal" tabindex="-1" aria-hidden="true" aria-labelledby="restoreModalLabel">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" id="restoreForm" action="/settings/database/backup/x/restore">
<input type="hidden" name="_csrf" value="{{$csrf}}">
<div class="modal-header bg-warning-subtle">
<h5 class="modal-title" id="restoreModalLabel">
<i class="bi bi-exclamation-triangle me-1"></i>Restore from backup
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
This replaces <strong>every</strong> zone, record, policy, list and setting with
the contents of <span class="mono" id="restoreName"></span>.
</p>
<p class="small text-body-secondary">
The swap happens on the next start, not immediately: overwriting the database
underneath a running process would leave open connections reading a file that no
longer exists. Your current database is preserved next to it, so this is
reversible.
</p>
<div class="mb-2">
<label class="form-label" for="restoreConfirm">
Type the file name to confirm
</label>
<input type="text" class="form-control mono" id="restoreConfirm" name="confirm"
required autocomplete="off" placeholder="vibedns-00000000-000000.db">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning">Stage restore</button>
</div>
</form>
</div>
</div>
</div>
{{end}}