276 lines
12 KiB
HTML
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}}
|