diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index ba29361..7177a6e 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -61,6 +61,10 @@ .metric { padding: 14px; } .metric .label { color: var(--muted); font-size: 12px; text-transform: uppercase; } .metric .value { font-size: 26px; font-weight: 650; margin-top: 4px; } + .metric.failed { border-color: #e8b4b4; background: #fff7f7; } + .metric.warning { border-color: #e7cf8a; background: #fffaf0; } + .metric.running { border-color: #e7cf8a; background: #fffaf0; } + .metric.queued { border-color: #b5cdea; background: #eef6ff; } .panel { padding: 16px; margin-bottom: 18px; overflow: auto; } .panel.highlight { border-left: 4px solid var(--border); } .panel.highlight.failed { border-left-color: var(--failed); background: #fff7f7; } @@ -118,6 +122,23 @@ cursor: not-allowed; } .inline-form { margin: 0; } + .status-overview { + display: grid; + gap: 8px; + } + .status-summary { + align-items: center; + border: 1px solid var(--border); + border-radius: 6px; + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 10px; + } + .status-summary.failed { border-color: #e8b4b4; background: #fff7f7; color: var(--failed); } + .status-summary.warning, + .status-summary.running { border-color: #e7cf8a; background: #fffaf0; color: var(--running); } + .status-summary.queued { border-color: #b5cdea; background: #eef6ff; color: var(--link); } .operator-state { align-items: center; display: flex; diff --git a/src/pobsync_backend/templates/pobsync_backend/dashboard.html b/src/pobsync_backend/templates/pobsync_backend/dashboard.html index e0299ce..b595b34 100644 --- a/src/pobsync_backend/templates/pobsync_backend/dashboard.html +++ b/src/pobsync_backend/templates/pobsync_backend/dashboard.html @@ -32,10 +32,46 @@
Schedules
{{ counts.enabled_schedules }}/{{ counts.schedules }}
Snapshots
{{ counts.snapshots }}
Runs
{{ counts.runs }}
-
Queued
{{ counts.queued_runs }}
-
Running
{{ counts.running_runs }}
-
Warnings
{{ counts.warning_runs }}
-
Failed
{{ counts.failed_runs }}
+
Queued
{{ counts.queued_runs }}
+
Running
{{ counts.running_runs }}
+
Warnings
{{ counts.warning_runs }}
+
Failed
{{ counts.failed_runs }}
+ + +
+

Operational Status

+ {% if counts.failed_runs or counts.warning_runs or counts.running_runs or counts.queued_runs %} +
+ {% if counts.failed_runs %} +
+ failed + {{ counts.failed_runs }} failed run{{ counts.failed_runs|pluralize }} need{{ counts.failed_runs|pluralize:"s," }} review. +
+ {% endif %} + {% if counts.warning_runs %} +
+ warning + {{ counts.warning_runs }} run{{ counts.warning_runs|pluralize }} completed with warnings. +
+ {% endif %} + {% if counts.running_runs %} +
+ running + {{ counts.running_runs }} backup run{{ counts.running_runs|pluralize }} in progress. +
+ {% endif %} + {% if counts.queued_runs %} +
+ queued + {{ counts.queued_runs }} backup run{{ counts.queued_runs|pluralize }} waiting for the worker. +
+ {% endif %} +
+ {% elif counts.hosts %} +

ok No queued, running, warning, or failed runs.

+ {% else %} +

Add a host to start tracking backup status here.

+ {% endif %}
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 64c79f1..5fcaa29 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -84,6 +84,11 @@ class ViewTests(TestCase): self.assertContains(response, "running 1") self.assertContains(response, "warning 1") self.assertContains(response, "failed 1") + self.assertContains(response, "Operational Status") + self.assertContains(response, "1 failed run needs review.") + self.assertContains(response, "1 run completed with warnings.") + self.assertContains(response, "1 backup run in progress.") + self.assertContains(response, "1 backup run waiting for the worker.") def test_dashboard_renders_backup_trend_summary(self) -> None: self.client.force_login(self.staff_user) @@ -148,6 +153,17 @@ class ViewTests(TestCase): self.assertContains(response, "No completed backup runs with stats yet.") self.assertContains(response, "growth estimates") + def test_dashboard_shows_all_clear_operational_status(self) -> None: + self.client.force_login(self.staff_user) + GlobalConfig.objects.create(name="default", backup_root="/opt/pobsync/backups") + HostConfig.objects.create(host="web-01", address="web-01.example.test") + + response = self.client.get(reverse("dashboard")) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Operational Status") + self.assertContains(response, "No queued, running, warning, or failed runs.") + def test_dashboard_surfaces_retention_warnings(self) -> None: self.client.force_login(self.staff_user) host = HostConfig.objects.create(