v0.11.9 UI Polish Fixes for backup section

- Fix 1: margin-bottom 1rem→1.5rem on .deploy-cross-drive
- Fix 2: info tooltip on "Módszer"; rename restic to "Titkosított mentés"
- Fix 3: replace disabled checkbox with green/gray dot status indicator
- Fix 4: progressive disclosure — dest/method/schedule selects disabled
  until "Engedélyezve" checked; backend preserves config when disabling
- Fix 5: remove all emoji from deploy.html and backups.html backup sections

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 16:12:06 +01:00
parent a56448f7c9
commit a4713c054b
6 changed files with 182 additions and 42 deletions
+10
View File
@@ -1,5 +1,15 @@
## Changelog ## Changelog
### What was just completed (2026-02-17 session 36)
- **v0.11.9 — UI Polish Fixes for deploy/settings backup section:**
- **Fix 1: Spacing** — `.deploy-cross-drive` `margin-bottom` increased from `1rem` to `1.5rem` for consistent spacing before deploy form.
- **Fix 2: Tooltip on "Módszer"** — Renamed "Verziózott mentés (restic)" to "Titkosított mentés (restic)". Added info `(i)` tooltip explaining rsync vs restic tradeoffs.
- **Fix 3: Nightly backup indicator** — Replaced disabled checkbox (with confusing pointer cursor) with a non-interactive green/gray dot indicator.
- **Fix 4: Progressive disclosure** — Dest/method/schedule selects are disabled until "Engedélyezve" is checked. JS `toggleCrossDriveFields()` enables/disables them. Backend handler updated to preserve existing config when disabling (disabled fields not submitted).
- **Fix 5: Emoji cleanup** — Removed all emoji from `deploy.html` backup section (h4, warning, status, hint, stale data) and `backups.html` cross-drive summary (status badges, schedule badge, unconfigured warning). JS callbacks also cleaned up.
- **CSS added:** `.info-tooltip`, `.info-icon`, `.info-tooltip-text`, `.cross-drive-nightly-status`, `.nightly-status-indicator`, `.nightly-enabled`, `.nightly-disabled`, `.meta-badge-fail`.
- **Files modified (4):** `web/templates/deploy.html`, `web/templates/backups.html`, `web/templates/style.css`, `web/handlers.go`
### What was just completed (2026-02-17 session 35) ### What was just completed (2026-02-17 session 35)
- **v0.11.8 — Per-App Cross-Drive Backup (3-2-1 rule, second copy on different media):** - **v0.11.8 — Per-App Cross-Drive Backup (3-2-1 rule, second copy on different media):**
- **Feature: CrossDriveBackup data model** — `AppBackupPrefs` extended with `CrossDrive *CrossDriveBackup` field in `settings.go`. New methods: `GetCrossDriveConfig`, `SetCrossDriveConfig`, `UpdateCrossDriveStatus`, `GetAllCrossDriveConfigs`, `GetOrCreateCrossDrivePassword`. Existing `SetAppBackup`/`SetAppBackupBulk` now preserve cross-drive config. Auto-generated restic password stored in `settings.json`. - **Feature: CrossDriveBackup data model** — `AppBackupPrefs` extended with `CrossDrive *CrossDriveBackup` field in `settings.go`. New methods: `GetCrossDriveConfig`, `SetCrossDriveConfig`, `UpdateCrossDriveStatus`, `GetAllCrossDriveConfigs`, `GetOrCreateCrossDrivePassword`. Existing `SetAppBackup`/`SetAppBackupBulk` now preserve cross-drive config. Auto-generated restic password stored in `settings.json`.
+2 -1
View File
@@ -20,7 +20,7 @@ Last updated: 2026-02-17 (session 35)
- Customer deployments use Docker Compose (not Kubernetes) for simplicity - Customer deployments use Docker Compose (not Kubernetes) for simplicity
### felhom-controller (this repo) ### felhom-controller (this repo)
- **Version:** v0.11.8 - **Version:** v0.11.9
- **Phase 1:** ✅ COMPLETE — Stack Manager + Deploy Flow - **Phase 1:** ✅ COMPLETE — Stack Manager + Deploy Flow
- **Phase 2:** ✅ COMPLETE — Monitoring & Health (scheduler, CPU/temp, healthchecks.io pings) - **Phase 2:** ✅ COMPLETE — Monitoring & Health (scheduler, CPU/temp, healthchecks.io pings)
- **Phase 3:** ✅ COMPLETE — Backups (DB dumps, restic integration, manual trigger, **dedicated backup page**) - **Phase 3:** ✅ COMPLETE — Backups (DB dumps, restic integration, manual trigger, **dedicated backup page**)
@@ -38,6 +38,7 @@ Last updated: 2026-02-17 (session 35)
- **v0.11.6:** ✅ COMPLETE — FileBrowser auto-mount sync (`syncFileBrowserMounts()`) + 3 UI fixes (badge color, progress bar, button text) - **v0.11.6:** ✅ COMPLETE — FileBrowser auto-mount sync (`syncFileBrowserMounts()`) + 3 UI fixes (badge color, progress bar, button text)
- **v0.11.7:** ✅ COMPLETE — Stale data cleanup + FileBrowser sync after migration + deploy page title fix - **v0.11.7:** ✅ COMPLETE — Stale data cleanup + FileBrowser sync after migration + deploy page title fix
- **v0.11.8:** ✅ COMPLETE — Per-App Cross-Drive Backup (3-2-1 rule): rsync/restic to secondary drive, deploy page UI, backup page summary, scheduler jobs, API endpoints - **v0.11.8:** ✅ COMPLETE — Per-App Cross-Drive Backup (3-2-1 rule): rsync/restic to secondary drive, deploy page UI, backup page summary, scheduler jobs, API endpoints
- **v0.11.9:** ✅ COMPLETE — UI Polish Fixes: spacing, tooltip on "Módszer", status dot instead of disabled checkbox, progressive disclosure, emoji cleanup
- **First app deployed:** Paperless-ngx on demo-felhom.eu (2026-02-13) - **First app deployed:** Paperless-ngx on demo-felhom.eu (2026-02-13)
- **Running on:** demo-felhom (N100 mini PC) at 192.168.0.162:8080 - **Running on:** demo-felhom (N100 mini PC) at 192.168.0.162:8080
- **All Phase 1-5 features working:** deploy, start/stop/restart/update, logs, health-aware states, auth, monitoring, backups, backup detail page, system monitoring page, settings page - **All Phase 1-5 features working:** deploy, start/stop/restart/update, logs, health-aware states, auth, monitoring, backups, backup detail page, system monitoring page, settings page
+21 -13
View File
@@ -519,23 +519,31 @@ func (s *Server) settingsCrossBackupHandler(w http.ResponseWriter, r *http.Reque
_ = r.ParseForm() _ = r.ParseForm()
enabled := r.FormValue("cross_drive_enabled") == "on" enabled := r.FormValue("cross_drive_enabled") == "on"
method := r.FormValue("cross_drive_method")
destPath := r.FormValue("cross_drive_dest")
schedule := r.FormValue("cross_drive_schedule")
// Validate method and schedule // Preserve existing runtime status fields and config when disabling
if method != "rsync" && method != "restic" {
method = "rsync"
}
if schedule != "daily" && schedule != "weekly" && schedule != "manual" {
schedule = "daily"
}
// Preserve existing runtime status fields
existing := s.settings.GetCrossDriveConfig(name) existing := s.settings.GetCrossDriveConfig(name)
var method, destPath, schedule string
if enabled {
method = r.FormValue("cross_drive_method")
destPath = r.FormValue("cross_drive_dest")
schedule = r.FormValue("cross_drive_schedule")
// Validate method and schedule
if method != "rsync" && method != "restic" {
method = "rsync"
}
if schedule != "daily" && schedule != "weekly" && schedule != "manual" {
schedule = "daily"
}
} else if existing != nil {
// Preserve existing settings when disabling
method = existing.Method
destPath = existing.DestinationPath
schedule = existing.Schedule
}
var cfg *settings.CrossDriveBackup var cfg *settings.CrossDriveBackup
if destPath != "" { if destPath != "" || existing != nil {
cfg = &settings.CrossDriveBackup{ cfg = &settings.CrossDriveBackup{
Enabled: enabled, Enabled: enabled,
Method: method, Method: method,
@@ -307,10 +307,10 @@
<span class="meta-badge">{{.MethodLabel}}</span> <span class="meta-badge">{{.MethodLabel}}</span>
{{if .DestLabel}}<span class="meta-badge meta-badge-storage">→ {{.DestLabel}}</span> {{if .DestLabel}}<span class="meta-badge meta-badge-storage">→ {{.DestLabel}}</span>
{{else if .DestPath}}<span class="meta-badge meta-badge-storage">→ {{.DestPath}}</span>{{end}} {{else if .DestPath}}<span class="meta-badge meta-badge-storage">→ {{.DestPath}}</span>{{end}}
{{if eq .LastStatus "ok"}}<span class="meta-badge meta-badge-ok">{{.LastRunShort}}</span> {{if eq .LastStatus "ok"}}<span class="meta-badge meta-badge-ok">{{.LastRunShort}}</span>
{{else if eq .LastStatus "error"}}<span class="meta-badge meta-badge-fail">Hiba</span> {{else if eq .LastStatus "error"}}<span class="meta-badge meta-badge-fail">Hiba</span>
{{else if eq .LastStatus "running"}}<span class="meta-badge">Fut...</span> {{else if eq .LastStatus "running"}}<span class="meta-badge">Fut...</span>
{{else}}<span class="meta-badge" style="color:var(--text-muted)">{{.ScheduleLabel}}</span>{{end}} {{else}}<span class="meta-badge" style="color:var(--text-muted)">{{.ScheduleLabel}}</span>{{end}}
{{if .SizeHuman}}<span class="mono" style="font-size:.8rem;color:var(--text-muted)">{{.SizeHuman}}</span>{{end}} {{if .SizeHuman}}<span class="mono" style="font-size:.8rem;color:var(--text-muted)">{{.SizeHuman}}</span>{{end}}
</div> </div>
</div> </div>
@@ -321,7 +321,7 @@
{{if .Backup.UnconfiguredApps}} {{if .Backup.UnconfiguredApps}}
<div style="font-size:.85rem;color:var(--yellow);margin-bottom:1rem"> <div style="font-size:.85rem;color:var(--yellow);margin-bottom:1rem">
⚠️ {{len .Backup.UnconfiguredApps}} alkalmazáshoz nincs beállítva: {{len .Backup.UnconfiguredApps}} alkalmazáshoz nincs beállítva:
{{range .Backup.UnconfiguredApps}} {{range .Backup.UnconfiguredApps}}
<a href="/stacks/{{.StackName}}/deploy" style="color:var(--accent-blue)">{{.DisplayName}}</a> <a href="/stacks/{{.StackName}}/deploy" style="color:var(--accent-blue)">{{.DisplayName}}</a>
{{end}} {{end}}
@@ -495,7 +495,7 @@ function triggerAllCrossDrive(btn) {
btn.textContent = 'Összes futtatása most'; btn.textContent = 'Összes futtatása most';
return; return;
} }
btn.textContent = 'Mentések futnak...'; btn.textContent = 'Mentések futnak...';
setTimeout(function() { location.reload(); }, 5000); setTimeout(function() { location.reload(); }, 5000);
}) })
.catch(function(e) { .catch(function(e) {
+50 -21
View File
@@ -62,7 +62,7 @@
{{end}} {{end}}
{{if .StaleData}} {{if .StaleData}}
<div class="deploy-stale-data"> <div class="deploy-stale-data">
<h4>🗑️ Korábbi adatok</h4> <h4>Korábbi adatok</h4>
<p class="form-hint" style="margin-bottom:1rem"> <p class="form-hint" style="margin-bottom:1rem">
Az alkalmazás adatainak másolata megtalálható egy másik tárolón is. Az alkalmazás adatainak másolata megtalálható egy másik tárolón is.
Ez általában áthelyezés után marad hátra. Ez általában áthelyezés után marad hátra.
@@ -84,7 +84,7 @@
</div> </div>
</div> </div>
<button class="btn btn-sm btn-danger" onclick="deleteStaleData('{{$.Meta.Slug}}', '{{.Path}}', this)"> <button class="btn btn-sm btn-danger" onclick="deleteStaleData('{{$.Meta.Slug}}', '{{.Path}}', this)">
🗑️ Korábbi adatok törlése Korábbi adatok törlése
</button> </button>
</div> </div>
{{end}} {{end}}
@@ -95,13 +95,17 @@
{{if .AlreadyDeployed}} {{if .AlreadyDeployed}}
{{if .StorageInfo}} {{if .StorageInfo}}
<div class="deploy-cross-drive"> <div class="deploy-cross-drive">
<h4>🔒 Biztonsági mentés</h4> <h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
<label class="toggle"> <div class="cross-drive-nightly-status">
<input type="checkbox" id="app-backup-enabled" {{if .AppBackupEnabled}}checked{{end}} disabled> {{if .AppBackupEnabled}}
<span class="nightly-status-indicator nightly-enabled"></span>
{{else}}
<span class="nightly-status-indicator nightly-disabled"></span>
{{end}}
<span class="toggle-label">Napi mentésbe foglalás (restic, helyi)</span> <span class="toggle-label">Napi mentésbe foglalás (restic, helyi)</span>
</label> </div>
<span class="form-hint" style="display:block;margin-top:.25rem"> <span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatai bekerülnek az éjszakai biztonsági mentésbe. Az alkalmazás adatai bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--accent-blue)">Beállítás a mentési oldalon</a> <a href="/backups" style="color:var(--accent-blue)">Beállítás a mentési oldalon</a>
@@ -113,7 +117,7 @@
<p style="font-weight:500;margin-bottom:1rem">Másolat másik meghajtóra:</p> <p style="font-weight:500;margin-bottom:1rem">Másolat másik meghajtóra:</p>
{{if .BackupDestWarning}} {{if .BackupDestWarning}}
<div class="alert alert-warning" style="margin-bottom:1rem">⚠️ {{.BackupDestWarning}}</div> <div class="alert alert-warning" style="margin-bottom:1rem">{{.BackupDestWarning}}</div>
{{end}} {{end}}
{{if not .BackupDestPaths}} {{if not .BackupDestPaths}}
@@ -128,13 +132,15 @@
<span class="settings-label">Engedélyezve</span> <span class="settings-label">Engedélyezve</span>
<label class="toggle" style="margin:0"> <label class="toggle" style="margin:0">
<input type="checkbox" name="cross_drive_enabled" id="cross-drive-enabled" <input type="checkbox" name="cross_drive_enabled" id="cross-drive-enabled"
{{if and .CrossDriveConfig .CrossDriveConfig.Enabled}}checked{{end}}> {{if and .CrossDriveConfig .CrossDriveConfig.Enabled}}checked{{end}}
onchange="toggleCrossDriveFields()">
<span class="toggle-label">Igen</span> <span class="toggle-label">Igen</span>
</label> </label>
</div> </div>
<div class="settings-row"> <div class="settings-row">
<span class="settings-label">Cél tárhely</span> <span class="settings-label">Cél tárhely</span>
<select name="cross_drive_dest" class="form-control" style="max-width:20rem"> <select name="cross_drive_dest" id="cd-dest" class="form-control cross-drive-field" style="max-width:20rem"
{{if not (and .CrossDriveConfig .CrossDriveConfig.Enabled)}}disabled{{end}}>
{{range .BackupDestPaths}} {{range .BackupDestPaths}}
<option value="{{.Path}}" <option value="{{.Path}}"
{{if and $.CrossDriveConfig (eq $.CrossDriveConfig.DestinationPath .Path)}}selected{{end}}> {{if and $.CrossDriveConfig (eq $.CrossDriveConfig.DestinationPath .Path)}}selected{{end}}>
@@ -145,19 +151,34 @@
</select> </select>
</div> </div>
<div class="settings-row"> <div class="settings-row">
<span class="settings-label">Módszer</span> <span class="settings-label">
<select name="cross_drive_method" class="form-control" style="max-width:20rem"> Módszer
<span class="info-tooltip" tabindex="0">
<span class="info-icon">i</span>
<span class="info-tooltip-text">
<strong>Egyszerű másolat (rsync):</strong> Tükörszerű másolat, a fájlok közvetlenül böngészhetők.
Nem titkosított, nem verziózott — mindig a legfrissebb állapotot tartalmazza.
<br><br>
<strong>Titkosított mentés (restic):</strong> Titkosított, tömörített, verziózott mentés.
Korábbi állapotok visszaállíthatók. Nem böngészhető közvetlenül —
visszaállításhoz a vezérlőpult szükséges.
</span>
</span>
</span>
<select name="cross_drive_method" id="cd-method" class="form-control cross-drive-field" style="max-width:20rem"
{{if not (and .CrossDriveConfig .CrossDriveConfig.Enabled)}}disabled{{end}}>
<option value="rsync" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Method "rsync")}}selected{{end}}> <option value="rsync" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Method "rsync")}}selected{{end}}>
Egyszerű másolat (rsync) Egyszerű másolat (rsync)
</option> </option>
<option value="restic" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Method "restic")}}selected{{end}}> <option value="restic" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Method "restic")}}selected{{end}}>
Verziózott mentés (restic) Titkosított mentés (restic)
</option> </option>
</select> </select>
</div> </div>
<div class="settings-row"> <div class="settings-row">
<span class="settings-label">Ütemezés</span> <span class="settings-label">Ütemezés</span>
<select name="cross_drive_schedule" class="form-control" style="max-width:20rem"> <select name="cross_drive_schedule" id="cd-schedule" class="form-control cross-drive-field" style="max-width:20rem"
{{if not (and .CrossDriveConfig .CrossDriveConfig.Enabled)}}disabled{{end}}>
<option value="daily" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "daily")}}selected{{end}}> <option value="daily" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "daily")}}selected{{end}}>
Naponta (03:30) Naponta (03:30)
</option> </option>
@@ -175,7 +196,7 @@
{{if .CrossDriveConfig.LastRun}} {{if .CrossDriveConfig.LastRun}}
<div class="form-hint" style="margin-bottom:.75rem"> <div class="form-hint" style="margin-bottom:.75rem">
Utolsó futás: {{.CrossDriveConfig.LastRun}} Utolsó futás: {{.CrossDriveConfig.LastRun}}
{{if eq .CrossDriveConfig.LastStatus "ok"}}Sikeres{{else if eq .CrossDriveConfig.LastStatus "error"}}Hiba: {{.CrossDriveConfig.LastError}}{{else if eq .CrossDriveConfig.LastStatus "running"}}Fut...{{end}} {{if eq .CrossDriveConfig.LastStatus "ok"}}Sikeres{{else if eq .CrossDriveConfig.LastStatus "error"}}Hiba: {{.CrossDriveConfig.LastError}}{{else if eq .CrossDriveConfig.LastStatus "running"}}Fut...{{end}}
{{if .CrossDriveConfig.LastDuration}} ({{.CrossDriveConfig.LastDuration}}){{end}} {{if .CrossDriveConfig.LastDuration}} ({{.CrossDriveConfig.LastDuration}}){{end}}
{{if .CrossDriveConfig.LastSizeHuman}} — {{.CrossDriveConfig.LastSizeHuman}}{{end}} {{if .CrossDriveConfig.LastSizeHuman}} — {{.CrossDriveConfig.LastSizeHuman}}{{end}}
</div> </div>
@@ -194,7 +215,7 @@
</form> </form>
<div class="form-hint" style="margin-top:.75rem;color:var(--text-muted)"> <div class="form-hint" style="margin-top:.75rem;color:var(--text-muted)">
⚠️ A cél meghajtó legyen más fizikai eszköz, mint az alkalmazás adattárolója. A cél meghajtó legyen más fizikai eszköz, mint az alkalmazás adattárolója.
</div> </div>
{{end}} {{end}}
</div> </div>
@@ -353,6 +374,14 @@
</div> </div>
<script> <script>
function toggleCrossDriveFields() {
var enabled = document.getElementById('cross-drive-enabled').checked;
var fields = document.querySelectorAll('.cross-drive-field');
for (var i = 0; i < fields.length; i++) {
fields[i].disabled = !enabled;
}
}
function triggerCrossDriveBackup(stackName, btn) { function triggerCrossDriveBackup(stackName, btn) {
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Mentés folyamatban...'; btn.textContent = 'Mentés folyamatban...';
@@ -365,7 +394,7 @@ function triggerCrossDriveBackup(stackName, btn) {
btn.textContent = 'Mentés most'; btn.textContent = 'Mentés most';
return; return;
} }
btn.textContent = 'Mentés folyamatban...'; btn.textContent = 'Mentés folyamatban...';
// Poll status // Poll status
var poll = setInterval(function() { var poll = setInterval(function() {
fetch('/api/stacks/' + stackName + '/cross-backup/status') fetch('/api/stacks/' + stackName + '/cross-backup/status')
@@ -376,9 +405,9 @@ function triggerCrossDriveBackup(stackName, btn) {
clearInterval(poll); clearInterval(poll);
var status = s.data.last_status; var status = s.data.last_status;
if (status === 'ok') { if (status === 'ok') {
btn.textContent = 'Mentés kész'; btn.textContent = 'Mentés kész';
} else { } else {
btn.textContent = 'Hiba'; btn.textContent = 'Hiba';
alert('Hiba: ' + (s.data.last_error || 'Ismeretlen hiba')); alert('Hiba: ' + (s.data.last_error || 'Ismeretlen hiba'));
} }
setTimeout(function() { location.reload(); }, 2000); setTimeout(function() { location.reload(); }, 2000);
@@ -439,12 +468,12 @@ function deleteStaleData(stackName, stalePath, btn) {
if (!data.ok) { if (!data.ok) {
alert('Hiba: ' + (data.error || 'Ismeretlen hiba')); alert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false; btn.disabled = false;
btn.textContent = '🗑️ Korábbi adatok törlése'; btn.textContent = 'Korábbi adatok törlése';
return; return;
} }
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?'); var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) { if (data.errors && data.errors.length > 0) {
msg += '\n\n⚠️ Néhány hiba történt:\n' + data.errors.join('\n'); msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
} }
alert(msg); alert(msg);
// Remove the stale data card from DOM // Remove the stale data card from DOM
+93 -1
View File
@@ -2274,7 +2274,7 @@ a.stat-card:hover {
border-radius: var(--radius); border-radius: var(--radius);
padding: 1.5rem; padding: 1.5rem;
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1.5rem;
} }
.deploy-cross-drive h4 { .deploy-cross-drive h4 {
@@ -2330,3 +2330,95 @@ a.stat-card:hover {
gap: .5rem; gap: .5rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
/* Info tooltip (i icon with hover popup) */
.info-tooltip {
position: relative;
display: inline-flex;
align-items: center;
margin-left: .35rem;
cursor: help;
}
.info-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid var(--text-muted);
color: var(--text-muted);
font-size: .65rem;
font-weight: 700;
font-style: italic;
font-family: Georgia, serif;
line-height: 1;
}
.info-tooltip-text {
display: none;
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
width: 320px;
padding: .75rem 1rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
font-size: .8rem;
font-weight: 400;
line-height: 1.5;
color: var(--text-secondary);
z-index: 100;
white-space: normal;
}
.info-tooltip:hover .info-tooltip-text,
.info-tooltip:focus .info-tooltip-text,
.info-tooltip:focus-within .info-tooltip-text {
display: block;
}
.info-tooltip-text::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: var(--border-color);
}
/* Nightly backup status indicator (non-interactive) */
.cross-drive-nightly-status {
display: flex;
align-items: center;
gap: .5rem;
}
.nightly-status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.nightly-enabled {
background: var(--green);
box-shadow: 0 0 4px rgba(35, 134, 54, 0.4);
}
.nightly-disabled {
background: var(--text-muted);
opacity: 0.5;
}
/* meta-badge-fail */
.meta-badge-fail {
background: var(--red-bg);
color: var(--red);
}