Phase 3 complete: per-app backup toggles, restore, storage overview
- Storage overview on backup page (SSD/HDD bars, repo stats) - Restic password visibility + hub sync for disaster recovery - App data discovery (HDD bind mounts, Docker volumes) - Per-app backup toggle checkboxes with settings persistence - Dynamic backup paths: enabled app HDD data included in restic snapshots - Limited app restore from snapshots (self-service recovery) - Snapshots API endpoint for restore dropdown - Version bump to 0.8.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -237,14 +237,31 @@ func isPingConfigured(uuid string) bool {
|
||||
return uuid != "" && !strings.HasPrefix(uuid, "CHANGEME")
|
||||
}
|
||||
|
||||
func (s *Server) backupsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.baseData("backups", "Biztonsági mentés")
|
||||
|
||||
// System info for storage overview bars
|
||||
data["SystemInfo"] = system.GetInfo(s.cfg.Paths.HDDPath, s.cpuCollector)
|
||||
|
||||
if s.backupMgr != nil {
|
||||
nextDBDump := scheduler.NextDailyRun(s.cfg.Backup.DBDumpSchedule)
|
||||
nextBackup := scheduler.NextDailyRun(s.cfg.Backup.ResticSchedule)
|
||||
fullStatus := s.backupMgr.GetFullStatus(nextDBDump, nextBackup)
|
||||
|
||||
// Pass flash messages from query params (set by redirect handlers)
|
||||
if flash := r.URL.Query().Get("flash"); flash != "" {
|
||||
fullStatus.FlashSuccess = flash
|
||||
}
|
||||
if flashErr := r.URL.Query().Get("flash_error"); flashErr != "" {
|
||||
fullStatus.FlashError = flashErr
|
||||
}
|
||||
|
||||
data["Backup"] = fullStatus
|
||||
|
||||
// Restic password for display
|
||||
if pw, err := s.backupMgr.GetResticPassword(); err == nil {
|
||||
data["ResticPassword"] = pw
|
||||
}
|
||||
} else {
|
||||
data["Backup"] = nil
|
||||
}
|
||||
@@ -252,6 +269,69 @@ func (s *Server) backupsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
s.render(w, "backups", data)
|
||||
}
|
||||
|
||||
func (s *Server) settingsAppBackupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
|
||||
if s.backupMgr == nil {
|
||||
http.Redirect(w, r, "/backups", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Get current app data info to know which stacks have HDD data
|
||||
nextDBDump := scheduler.NextDailyRun(s.cfg.Backup.DBDumpSchedule)
|
||||
nextBackup := scheduler.NextDailyRun(s.cfg.Backup.ResticSchedule)
|
||||
fullStatus := s.backupMgr.GetFullStatus(nextDBDump, nextBackup)
|
||||
|
||||
prefs := make(map[string]bool)
|
||||
for _, app := range fullStatus.AppDataInfo {
|
||||
if app.HasHDDData {
|
||||
prefs[app.StackName] = r.FormValue("backup_"+app.StackName) == "on"
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.settings.SetAppBackupBulk(prefs); err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to save app backup prefs: %v", err)
|
||||
http.Redirect(w, r, "/backups?flash_error=Hiba+a+ment%C3%A9skor", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] App backup preferences updated: %v", prefs)
|
||||
|
||||
// Trigger cache refresh so the page shows updated data
|
||||
go s.backupMgr.RefreshCache(nextDBDump, nextBackup)
|
||||
|
||||
http.Redirect(w, r, "/backups?flash=Alkalmaz%C3%A1s+ment%C3%A9si+be%C3%A1ll%C3%ADt%C3%A1sok+mentve.", http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
|
||||
stackName := r.FormValue("stack_name")
|
||||
snapshotID := r.FormValue("snapshot_id")
|
||||
|
||||
if stackName == "" || snapshotID == "" {
|
||||
http.Redirect(w, r, "/backups?flash_error=Hi%C3%A1nyz%C3%B3+param%C3%A9terek", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
if s.backupMgr == nil {
|
||||
http.Redirect(w, r, "/backups?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[WARN] Restore requested: stack=%s, snapshot=%s from %s", stackName, snapshotID, r.RemoteAddr)
|
||||
|
||||
if err := s.backupMgr.RestoreApp(stackName, snapshotID); err != nil {
|
||||
s.logger.Printf("[ERROR] Restore failed: %v", err)
|
||||
errMsg := url.QueryEscape("Visszaállítás sikertelen: " + err.Error())
|
||||
http.Redirect(w, r, "/backups?flash_error="+errMsg, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
msg := url.QueryEscape(stackName + " visszaállítva (" + snapshotID + ").")
|
||||
http.Redirect(w, r, "/backups?flash="+msg, http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) settingsData() map[string]interface{} {
|
||||
data := s.baseData("settings", "Beállítások")
|
||||
data["CustomerID"] = s.cfg.Customer.ID
|
||||
|
||||
@@ -94,6 +94,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.settingsNotificationsHandler(w, r)
|
||||
case path == "/settings/notifications/test" && r.Method == http.MethodPost:
|
||||
s.settingsNotificationsTestHandler(w, r)
|
||||
case path == "/settings/app-backup" && r.Method == http.MethodPost:
|
||||
s.settingsAppBackupHandler(w, r)
|
||||
case path == "/backup/restore" && r.Method == http.MethodPost:
|
||||
s.backupRestoreHandler(w, r)
|
||||
case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/logs"):
|
||||
name := strings.TrimPrefix(path, "/stacks/")
|
||||
name = strings.TrimSuffix(name, "/logs")
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
<span class="domain-badge">{{.Domain}}</span>
|
||||
</div>
|
||||
|
||||
{{if .Backup}}{{if .Backup.FlashSuccess}}
|
||||
<div class="flash flash-success">{{.Backup.FlashSuccess}}</div>
|
||||
{{end}}{{end}}
|
||||
{{if .Backup}}{{if .Backup.FlashError}}
|
||||
<div class="flash flash-error">{{.Backup.FlashError}}</div>
|
||||
{{end}}{{end}}
|
||||
|
||||
{{if not .Backup}}
|
||||
<div class="backup-empty-state">
|
||||
<div class="backup-empty-icon">🛡</div>
|
||||
@@ -15,6 +22,53 @@
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
<!-- Section 0: Storage overview -->
|
||||
<div class="backup-section-card">
|
||||
<h3>Tárhely áttekintés</h3>
|
||||
<div class="storage-overview-grid">
|
||||
<div class="storage-bars">
|
||||
{{with $.SystemInfo}}
|
||||
<div class="storage-item">
|
||||
<div class="storage-header">
|
||||
<span class="storage-label">SSD (/)</span>
|
||||
<span class="storage-value">{{fmtGB .DiskUsedGB}} / {{fmtGB .DiskTotalGB}} ({{printf "%.0f" .DiskPercent}}%)</span>
|
||||
</div>
|
||||
<div class="system-bar">
|
||||
<div class="system-bar-fill {{usageColor .DiskPercent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .DiskPercent}}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .HDDConfigured}}
|
||||
<div class="storage-item">
|
||||
<div class="storage-header">
|
||||
<span class="storage-label">Külső HDD</span>
|
||||
<span class="storage-value">{{fmtGB .HDDUsedGB}} / {{fmtGB .HDDTotalGB}} ({{printf "%.0f" .HDDPercent}}%)</span>
|
||||
</div>
|
||||
<div class="system-bar">
|
||||
<div class="system-bar-fill {{usageColor .HDDPercent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .HDDPercent}}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="storage-stats">
|
||||
{{if .Backup.RepoStats}}
|
||||
<div class="storage-stat-row">
|
||||
<span class="storage-stat-label">Mentési tároló</span>
|
||||
<span class="storage-stat-value mono">{{.Backup.RepoStats.TotalSize}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="storage-stat-row">
|
||||
<span class="storage-stat-label">DB mentések</span>
|
||||
<span class="storage-stat-value mono">{{if .Backup.DumpFiles}}{{len .Backup.DumpFiles}} fájl{{else}}–{{end}}</span>
|
||||
</div>
|
||||
<div class="storage-stat-row">
|
||||
<span class="storage-stat-label">Pillanatképek</span>
|
||||
<span class="storage-stat-value mono">{{if .Backup.RepoStats}}{{.Backup.RepoStats.SnapshotCount}}{{else}}0{{end}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 1: Status overview cards -->
|
||||
<div class="stats-grid backup-page-cards">
|
||||
{{if .Backup.LastBackup}}
|
||||
@@ -179,7 +233,56 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Section 4: Snapshots -->
|
||||
<!-- Section 4: App data backup toggles -->
|
||||
{{if .Backup.AppDataInfo}}
|
||||
<div class="backup-section-card">
|
||||
<h3>Alkalmazás adatok</h3>
|
||||
<p class="backup-section-desc">Az alkalmazások felhasználói adatainak biztonsági mentése.</p>
|
||||
<form method="POST" action="/settings/app-backup">
|
||||
<div class="app-backup-list">
|
||||
{{range .Backup.AppDataInfo}}
|
||||
<div class="app-backup-item">
|
||||
<div class="app-backup-header">
|
||||
{{if .HasHDDData}}
|
||||
<label class="app-backup-toggle">
|
||||
<input type="checkbox" name="backup_{{.StackName}}" value="on" {{if .BackupEnabled}}checked{{end}}>
|
||||
<span class="app-backup-name">{{.DisplayName}}</span>
|
||||
</label>
|
||||
{{else}}
|
||||
<div class="app-backup-toggle">
|
||||
<span class="app-backup-disabled-icon">—</span>
|
||||
<span class="app-backup-name">{{.DisplayName}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .HasHDDData}}
|
||||
<span class="app-backup-size mono">{{.HDDSizeHuman}} (HDD)</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="app-backup-details">
|
||||
{{range .HDDPaths}}
|
||||
<div class="app-backup-path mono">{{.HostPath}} {{if .Exists}}({{.SizeHuman}}){{else}}<span class="relative-time">(nem létezik)</span>{{end}}</div>
|
||||
{{end}}
|
||||
{{range .DockerVolumes}}
|
||||
<div class="app-backup-volume">Docker kötet: {{.Name}} <span class="relative-time">(nem mentett)</span></div>
|
||||
{{end}}
|
||||
{{if .HasDBDump}}
|
||||
<div class="app-backup-dbinfo">Adatbázis mentés naponta (DB dump)</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="app-backup-actions">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Mentés</button>
|
||||
</div>
|
||||
<div class="app-backup-notice">
|
||||
<span class="relative-time">Docker kötetek mentése jelenleg nem támogatott. Az adatbázisokat az automatikus DB dump menti naponta.</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Section 5: Snapshots -->
|
||||
<div class="backup-section-card">
|
||||
<h3>Pillanatképek</h3>
|
||||
{{if .Backup.SnapshotHistory}}
|
||||
@@ -216,7 +319,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Section 5: Repository -->
|
||||
<!-- Section 6: Repository -->
|
||||
<div class="repo-card">
|
||||
<h3>Tároló</h3>
|
||||
<div class="repo-info-rows">
|
||||
@@ -247,6 +350,22 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Encryption key -->
|
||||
{{if $.ResticPassword}}
|
||||
<div class="repo-encryption">
|
||||
<span class="repo-label">Titkosítási kulcs:</span>
|
||||
<div class="repo-encryption-row">
|
||||
<input type="password" id="restic-pw" class="restic-pw-field mono" value="{{$.ResticPassword}}" readonly>
|
||||
<button type="button" class="btn btn-sm" onclick="toggleResticPw()">Megjelenítés</button>
|
||||
<button type="button" class="btn btn-sm" onclick="copyResticPw()">Másolás</button>
|
||||
</div>
|
||||
<div class="repo-encryption-warn">
|
||||
Mentse el biztonságos helyre! A kulcs nélkül a biztonsági mentések NEM állíthatók vissza.
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="repo-paths">
|
||||
<span class="repo-label">Mentett útvonalak:</span>
|
||||
<ul class="repo-path-list">
|
||||
@@ -264,6 +383,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 7: Restore -->
|
||||
{{if .Backup.AppDataInfo}}
|
||||
<div class="backup-section-card">
|
||||
<h3>Visszaállítás</h3>
|
||||
<div class="restore-section">
|
||||
<div class="restore-form-row">
|
||||
<label class="restore-label">Alkalmazás:</label>
|
||||
<select id="restore-app" class="restore-select" onchange="onRestoreAppChange()">
|
||||
<option value="">— Válasszon —</option>
|
||||
{{range .Backup.AppDataInfo}}
|
||||
{{if and .HasHDDData .BackupEnabled}}
|
||||
<option value="{{.StackName}}" data-paths="{{range $i, $p := .HDDPaths}}{{if $i}},{{end}}{{$p.HostPath}}{{end}}">{{.DisplayName}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="restore-form-row">
|
||||
<label class="restore-label">Pillanatkép:</label>
|
||||
<select id="restore-snapshot" class="restore-select">
|
||||
<option value="">— Betöltés... —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="restore-paths" class="restore-paths" style="display:none;">
|
||||
<span class="restore-label">Visszaállítandó útvonalak:</span>
|
||||
<ul id="restore-paths-list" class="repo-path-list"></ul>
|
||||
</div>
|
||||
<div class="restore-warning">
|
||||
<strong>FIGYELMEZTETÉS</strong><br>
|
||||
A visszaállítás FELÜLÍRJA a kiválasztott alkalmazás jelenlegi adatait a mentés pillanatának állapotával.
|
||||
Ez a művelet NEM vonható vissza!<br>
|
||||
Javasoljuk az alkalmazás leállítását a visszaállítás előtt.
|
||||
</div>
|
||||
<div class="restore-confirm">
|
||||
<label>
|
||||
<input type="checkbox" id="restore-confirm-cb" onchange="onRestoreConfirmChange()">
|
||||
Megértettem, visszaállítás saját felelősségre.
|
||||
</label>
|
||||
</div>
|
||||
<div class="restore-actions">
|
||||
<button type="button" class="btn btn-sm btn-danger" id="restore-btn" disabled onclick="submitRestore()">Visszaállítás indítása</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
||||
|
||||
<script>
|
||||
@@ -303,10 +467,105 @@ function startBackupPolling() {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Restic password toggle/copy
|
||||
function toggleResticPw() {
|
||||
var el = document.getElementById('restic-pw');
|
||||
el.type = el.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
function copyResticPw() {
|
||||
var el = document.getElementById('restic-pw');
|
||||
navigator.clipboard.writeText(el.value).then(function() {
|
||||
var btn = event.target;
|
||||
btn.textContent = 'Másolva!';
|
||||
setTimeout(function() { btn.textContent = 'Másolás'; }, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// Restore section
|
||||
function onRestoreAppChange() {
|
||||
var sel = document.getElementById('restore-app');
|
||||
var opt = sel.options[sel.selectedIndex];
|
||||
var pathsDiv = document.getElementById('restore-paths');
|
||||
var pathsList = document.getElementById('restore-paths-list');
|
||||
|
||||
if (!opt.value) {
|
||||
pathsDiv.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
var paths = (opt.getAttribute('data-paths') || '').split(',').filter(Boolean);
|
||||
pathsList.innerHTML = '';
|
||||
paths.forEach(function(p) {
|
||||
var li = document.createElement('li');
|
||||
li.className = 'mono';
|
||||
li.textContent = p;
|
||||
pathsList.appendChild(li);
|
||||
});
|
||||
pathsDiv.style.display = 'block';
|
||||
|
||||
// Load snapshots
|
||||
fetch('/api/backup/snapshots')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var snapSel = document.getElementById('restore-snapshot');
|
||||
snapSel.innerHTML = '<option value="">— Válasszon —</option>';
|
||||
if (data.ok && data.data) {
|
||||
data.data.forEach(function(s) {
|
||||
var o = document.createElement('option');
|
||||
o.value = s.short_id;
|
||||
var t = new Date(s.time);
|
||||
o.textContent = s.short_id + ' — ' + t.toLocaleString('hu-HU');
|
||||
snapSel.appendChild(o);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('restore-confirm-cb').checked = false;
|
||||
document.getElementById('restore-btn').disabled = true;
|
||||
}
|
||||
|
||||
function onRestoreConfirmChange() {
|
||||
var cb = document.getElementById('restore-confirm-cb');
|
||||
var app = document.getElementById('restore-app').value;
|
||||
var snap = document.getElementById('restore-snapshot').value;
|
||||
document.getElementById('restore-btn').disabled = !(cb.checked && app && snap);
|
||||
}
|
||||
|
||||
function submitRestore() {
|
||||
var app = document.getElementById('restore-app').value;
|
||||
var snap = document.getElementById('restore-snapshot').value;
|
||||
if (!app || !snap) return;
|
||||
|
||||
var btn = document.getElementById('restore-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Visszaállítás folyamatban...';
|
||||
|
||||
var form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/backup/restore';
|
||||
|
||||
var f1 = document.createElement('input');
|
||||
f1.type = 'hidden'; f1.name = 'stack_name'; f1.value = app;
|
||||
form.appendChild(f1);
|
||||
|
||||
var f2 = document.createElement('input');
|
||||
f2.type = 'hidden'; f2.name = 'snapshot_id'; f2.value = snap;
|
||||
form.appendChild(f2);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// Auto-poll if backup is already running on page load
|
||||
{{if .Backup}}{{if .Backup.Running}}
|
||||
startBackupPolling();
|
||||
{{end}}{{end}}
|
||||
|
||||
// Wire up snapshot selection change for restore confirm
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var snapSel = document.getElementById('restore-snapshot');
|
||||
if (snapSel) snapSel.addEventListener('change', onRestoreConfirmChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
{{template "layout_end" .}}
|
||||
|
||||
@@ -1769,6 +1769,213 @@ a.stat-card:hover {
|
||||
border-left: 3px solid var(--accent-blue);
|
||||
}
|
||||
|
||||
/* --- Backup page: Storage overview grid --- */
|
||||
.storage-overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
.storage-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
}
|
||||
.storage-stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: .3rem 0;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.storage-stat-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.storage-stat-value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* --- Backup page: App backup toggles --- */
|
||||
.backup-section-desc {
|
||||
color: var(--text-secondary);
|
||||
font-size: .85rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.app-backup-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.app-backup-item {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: .75rem 1rem;
|
||||
}
|
||||
.app-backup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
.app-backup-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.app-backup-toggle input[type="checkbox"] {
|
||||
accent-color: var(--accent-blue);
|
||||
}
|
||||
.app-backup-name {
|
||||
font-weight: 500;
|
||||
font-size: .9rem;
|
||||
}
|
||||
.app-backup-disabled-icon {
|
||||
color: var(--text-muted);
|
||||
font-size: .9rem;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.app-backup-size {
|
||||
font-size: .8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.app-backup-details {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.app-backup-path {
|
||||
font-size: .8rem;
|
||||
color: var(--text-secondary);
|
||||
padding: .1rem 0;
|
||||
}
|
||||
.app-backup-volume {
|
||||
font-size: .8rem;
|
||||
color: var(--text-muted);
|
||||
padding: .1rem 0;
|
||||
}
|
||||
.app-backup-dbinfo {
|
||||
font-size: .8rem;
|
||||
color: var(--text-muted);
|
||||
padding: .1rem 0;
|
||||
}
|
||||
.app-backup-actions {
|
||||
margin-top: .75rem;
|
||||
}
|
||||
.app-backup-notice {
|
||||
margin-top: .75rem;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
/* --- Backup page: Encryption key --- */
|
||||
.repo-encryption {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: .75rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.repo-encryption-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.restic-pw-field {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: .4rem .6rem;
|
||||
color: var(--text-primary);
|
||||
font-size: .85rem;
|
||||
width: 340px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.repo-encryption-warn {
|
||||
font-size: .8rem;
|
||||
color: var(--yellow);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* --- Backup page: Restore section --- */
|
||||
.restore-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .75rem;
|
||||
}
|
||||
.restore-form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
}
|
||||
.restore-label {
|
||||
font-size: .85rem;
|
||||
color: var(--text-secondary);
|
||||
min-width: 110px;
|
||||
}
|
||||
.restore-select {
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
padding: .4rem .6rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: .85rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
.restore-select option {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.restore-paths {
|
||||
padding: .5rem 0;
|
||||
}
|
||||
.restore-warning {
|
||||
background: var(--red-bg);
|
||||
border: 1px solid rgba(218, 54, 51, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: .75rem 1rem;
|
||||
font-size: .85rem;
|
||||
color: var(--red);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.restore-confirm {
|
||||
font-size: .85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.restore-confirm label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.restore-confirm input[type="checkbox"] {
|
||||
accent-color: var(--red);
|
||||
}
|
||||
.restore-actions {
|
||||
padding-top: .25rem;
|
||||
}
|
||||
|
||||
/* --- Flash messages --- */
|
||||
.flash {
|
||||
padding: .75rem 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: .85rem;
|
||||
border: 1px solid;
|
||||
}
|
||||
.flash-success {
|
||||
background: var(--green-bg);
|
||||
color: var(--green);
|
||||
border-color: rgba(35, 134, 54, 0.3);
|
||||
}
|
||||
.flash-error {
|
||||
background: var(--red-bg);
|
||||
color: var(--red);
|
||||
border-color: rgba(218, 54, 51, 0.3);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media(max-width: 768px) {
|
||||
.sidebar { width: 100%; height: auto; position: relative; border-right: none; border-bottom: 1px solid var(--border-color); }
|
||||
@@ -1786,4 +1993,8 @@ a.stat-card:hover {
|
||||
.charts-grid { grid-template-columns: 1fr; }
|
||||
.container-charts-row { flex-direction: column; }
|
||||
.sysinfo-grid { grid-template-columns: 1fr; }
|
||||
.storage-overview-grid { grid-template-columns: 1fr; }
|
||||
.restore-form-row { flex-direction: column; align-items: stretch; }
|
||||
.restore-label { min-width: auto; }
|
||||
.restore-select { max-width: 100%; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user