From 864a40e86229fc5feb1fd76b2412579d72e5b335 Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 21 May 2026 13:27:39 +0200 Subject: [PATCH] (ui) Surface storage pressure in dashboard priorities Move backup root usage, runway, daily new data, and available capacity into the top dashboard priority area so storage risk is visible before deeper trend details. Refs #27 --- .../templates/pobsync_backend/base.html | 30 ++++++++- .../templates/pobsync_backend/dashboard.html | 65 ++++++++++++++----- src/pobsync_backend/tests/test_views.py | 4 +- src/pobsync_backend/views.py | 16 ++--- 4 files changed, 86 insertions(+), 29 deletions(-) diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index c9b4ea1..beef593 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -288,7 +288,7 @@ .dashboard-priority-grid { display: grid; gap: 14px; - grid-template-columns: minmax(280px, 1.3fr) repeat(2, minmax(240px, 1fr)); + grid-template-columns: minmax(280px, 1.25fr) repeat(3, minmax(220px, 1fr)); margin-bottom: 20px; } .priority-panel { @@ -342,6 +342,34 @@ .schedule-time { justify-items: end; text-align: right; + } + .storage-priority { + display: grid; + gap: 12px; + } + .storage-priority .label { + color: var(--muted); + font-size: 12px; + font-weight: 650; + text-transform: uppercase; + } + .storage-priority .value { + font-size: 27px; + font-weight: 760; + line-height: 1.15; + margin-top: 4px; + } + .storage-priority-facts { + display: grid; + gap: 8px; + } + .storage-priority-facts > div { + align-items: baseline; + border-top: 1px solid var(--border); + display: flex; + gap: 10px; + justify-content: space-between; + padding-top: 8px; } .status-summary { align-items: center; diff --git a/src/pobsync_backend/templates/pobsync_backend/dashboard.html b/src/pobsync_backend/templates/pobsync_backend/dashboard.html index 2c44ff3..97324f8 100644 --- a/src/pobsync_backend/templates/pobsync_backend/dashboard.html +++ b/src/pobsync_backend/templates/pobsync_backend/dashboard.html @@ -114,6 +114,53 @@

No backup runs recorded yet.

{% endif %} + +
+

Storage Pressure

+ {% if stats_summary.runs_sampled %} +
+
+
Backup root used
+
+ {% if stats_summary.capacity.used_percent is not None %} + {{ stats_summary.capacity.used_percent|floatformat:1 }}% + {% else %} + unknown + {% endif %} +
+ {% if stats_summary.capacity.used_percent is not None %} +
+ +
+ {% endif %} +
+
+
+ Runway + + {% if stats_summary.estimated_days_until_full %} + {{ stats_summary.estimated_days_until_full }} days + {% elif stats_summary.estimated_runs_until_full %} + {{ stats_summary.estimated_runs_until_full }} runs + {% else %} + unknown + {% endif %} + +
+
+ New data + {{ stats_summary.avg_daily_literal_data_bytes|filesizeformat }}/day +
+
+ Available + {{ stats_summary.capacity.available_bytes|filesizeformat }} +
+
+
+ {% else %} +

Storage pressure appears after the first completed backup with stats.

+ {% endif %} +
@@ -129,24 +176,6 @@

Backup Trends

{% if stats_summary.runs_sampled %}
-
-
Storage Used
-
- {% if stats_summary.capacity.used_percent is not None %} - {{ stats_summary.capacity.used_percent|floatformat:1 }}% - {% else %} - unknown - {% endif %} -
- {% if stats_summary.capacity.used_percent is not None %} -
- -
- {% endif %} -
- {{ stats_summary.capacity.available_bytes|filesizeformat }} available from the backup root. -
-
Runway
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 118fcf7..e026558 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -177,14 +177,14 @@ class ViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "Backup Trends") - self.assertContains(response, "Storage Used") + self.assertContains(response, "Storage Pressure") + self.assertContains(response, "Backup root used") self.assertContains(response, "Runway") self.assertContains(response, "New Data") self.assertContains(response, "Link-Dest Savings") self.assertContains(response, "80.0%") self.assertContains(response, "10 days") self.assertContains(response, "Warnings") - self.assertContains(response, "Queued") self.assertContains(response, "Next Run") self.assertContains(response, "UTC") self.assertContains(response, "10") diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index ae0d91c..d6187cc 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -135,7 +135,7 @@ def _dashboard_action_items(hosts: list[HostConfig]) -> list[dict[str, object]]: "url": _runs_list_url(host=host_config.host, status="warning", review="needed"), } ) - if host_config.retention_warning.has_warning: + if host_config.retention_warning.get("has_warning"): action_items.append( { "host": host_config, @@ -168,15 +168,15 @@ def _dashboard_next_schedule_rows() -> list[dict[str, object]]: def _retention_warning_summary(retention_warning) -> str: parts = [] - if retention_warning.prune_exceeded: + if retention_warning.get("prune_exceeded"): parts.append( - f"Scheduled prune would delete {retention_warning.delete_count} snapshot(s), " - f"above max {retention_warning.max_delete}." + f"Scheduled prune would delete {retention_warning.get('delete_count')} snapshot(s), " + f"above max {retention_warning.get('max_delete')}." ) - if retention_warning.incomplete_count: - parts.append(f"{retention_warning.incomplete_count} incomplete snapshot(s) need review.") - if retention_warning.error: - parts.append(str(retention_warning.error)) + if retention_warning.get("incomplete_count"): + parts.append(f"{retention_warning.get('incomplete_count')} incomplete snapshot(s) need review.") + if retention_warning.get("error"): + parts.append(str(retention_warning.get("error"))) return " ".join(parts)