(ui) Add dashboard operational status summary
Make queued, running, warning, and failed run states more visible at the top of the dashboard with contextual status summaries and highlighted summary metrics. Also show an all-clear message when configured hosts have no active or problematic runs.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -32,10 +32,46 @@
|
||||
<div class="metric"><div class="label">Schedules</div><div class="value">{{ counts.enabled_schedules }}/{{ counts.schedules }}</div></div>
|
||||
<div class="metric"><div class="label">Snapshots</div><div class="value">{{ counts.snapshots }}</div></div>
|
||||
<div class="metric"><div class="label">Runs</div><div class="value">{{ counts.runs }}</div></div>
|
||||
<div class="metric"><div class="label">Queued</div><div class="value">{{ counts.queued_runs }}</div></div>
|
||||
<div class="metric"><div class="label">Running</div><div class="value">{{ counts.running_runs }}</div></div>
|
||||
<div class="metric"><div class="label">Warnings</div><div class="value">{{ counts.warning_runs }}</div></div>
|
||||
<div class="metric"><div class="label">Failed</div><div class="value">{{ counts.failed_runs }}</div></div>
|
||||
<div class="metric {% if counts.queued_runs %}queued{% endif %}"><div class="label">Queued</div><div class="value">{{ counts.queued_runs }}</div></div>
|
||||
<div class="metric {% if counts.running_runs %}running{% endif %}"><div class="label">Running</div><div class="value">{{ counts.running_runs }}</div></div>
|
||||
<div class="metric {% if counts.warning_runs %}warning{% endif %}"><div class="label">Warnings</div><div class="value">{{ counts.warning_runs }}</div></div>
|
||||
<div class="metric {% if counts.failed_runs %}failed{% endif %}"><div class="label">Failed</div><div class="value">{{ counts.failed_runs }}</div></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Operational Status</h2>
|
||||
{% if counts.failed_runs or counts.warning_runs or counts.running_runs or counts.queued_runs %}
|
||||
<div class="status-overview">
|
||||
{% if counts.failed_runs %}
|
||||
<div class="status-summary failed">
|
||||
<span class="status failed">failed</span>
|
||||
<strong>{{ counts.failed_runs }} failed run{{ counts.failed_runs|pluralize }} need{{ counts.failed_runs|pluralize:"s," }} review.</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if counts.warning_runs %}
|
||||
<div class="status-summary warning">
|
||||
<span class="status warning">warning</span>
|
||||
<strong>{{ counts.warning_runs }} run{{ counts.warning_runs|pluralize }} completed with warnings.</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if counts.running_runs %}
|
||||
<div class="status-summary running">
|
||||
<span class="status running">running</span>
|
||||
<strong>{{ counts.running_runs }} backup run{{ counts.running_runs|pluralize }} in progress.</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if counts.queued_runs %}
|
||||
<div class="status-summary queued">
|
||||
<span class="status queued">queued</span>
|
||||
<strong>{{ counts.queued_runs }} backup run{{ counts.queued_runs|pluralize }} waiting for the worker.</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif counts.hosts %}
|
||||
<p><span class="status ok">ok</span> No queued, running, warning, or failed runs.</p>
|
||||
{% else %}
|
||||
<p class="muted">Add a host to start tracking backup status here.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user