(ui) Improve dashboard host card scanability

Add per-host status chips for queued, running, warning, and failed runs so
the dashboard shows operational pressure without needing to open each host.

Restructure host cards into clearer backup activity and snapshot health
sections, with less visual clutter and better mobile wrapping.
This commit is contained in:
2026-05-21 01:41:45 +02:00
parent ef1761385e
commit a0fd33fcb8
4 changed files with 125 additions and 68 deletions

View File

@@ -176,23 +176,42 @@
overflow-wrap: anywhere;
}
.host-card-status {
flex: 0 0 auto;
display: flex;
flex: 0 1 auto;
flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
max-width: 50%;
}
.host-card-layout {
display: grid;
gap: 18px;
grid-template-columns: minmax(0, 1fr) minmax(240px, 320px);
gap: 24px;
grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr);
}
.host-card-section {
align-content: start;
display: grid;
gap: 10px;
min-width: 0;
}
.host-card-section-title {
color: var(--muted);
font-size: 12px;
font-weight: 650;
text-transform: uppercase;
}
.host-card-timeline {
display: grid;
gap: 12px;
gap: 16px 22px;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.host-card-stats {
align-content: start;
display: grid;
gap: 10px;
border-top: 1px solid #e6edf4;
gap: 12px 18px;
grid-template-columns: repeat(2, minmax(0, 1fr));
padding-top: 12px;
}
.host-card-item {
display: grid;
@@ -209,13 +228,9 @@
overflow-wrap: anywhere;
}
.host-card-stat {
background: #f8fafc;
border: 1px solid #e6edf4;
border-radius: 6px;
display: grid;
gap: 3px;
min-width: 0;
padding: 10px;
}
.host-card-stat .label {
color: var(--muted);
@@ -286,7 +301,10 @@
main { padding: 16px; }
nav { padding: 0; }
.two-col { grid-template-columns: 1fr; }
.host-card-header { display: grid; }
.host-card-status { justify-content: flex-start; max-width: none; }
.host-card-layout { grid-template-columns: 1fr; }
.host-card-stats { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
}
</style>
</head>

View File

@@ -63,9 +63,23 @@
</div>
<div class="host-card-status">
<span class="status {% if host.enabled %}ok{% else %}skipped{% endif %}">{{ host.enabled|yesno:"enabled,disabled" }}</span>
{% if host.queued_run_count %}
<span class="status queued">queued {{ host.queued_run_count }}</span>
{% endif %}
{% if host.running_run_count %}
<span class="status running">running {{ host.running_run_count }}</span>
{% endif %}
{% if host.warning_run_count %}
<span class="status warning">warning {{ host.warning_run_count }}</span>
{% endif %}
{% if host.failed_run_count %}
<span class="status failed">failed {{ host.failed_run_count }}</span>
{% endif %}
</div>
</div>
<div class="host-card-layout">
<div class="host-card-section">
<div class="host-card-section-title">Backup activity</div>
<div class="host-card-timeline">
<div class="host-card-item">
<div class="label">Latest Snapshot</div>
@@ -113,6 +127,9 @@
</div>
</div>
</div>
</div>
<div class="host-card-section">
<div class="host-card-section-title">Snapshot health</div>
<div class="host-card-stats">
<div class="host-card-stat">
<div class="label">Snapshots</div>
@@ -132,6 +149,7 @@
</div>
</div>
</div>
</div>
{% if host.retention_warning.has_warning %}
<div class="host-card-warning">
<span class="status warning">retention</span>

View File

@@ -55,6 +55,14 @@ class ViewTests(TestCase):
},
},
)
BackupRun.objects.create(host=host, status=BackupRun.Status.QUEUED)
BackupRun.objects.create(host=host, status=BackupRun.Status.RUNNING)
BackupRun.objects.create(
host=host,
run_type=BackupRun.RunType.MANUAL,
status=BackupRun.Status.FAILED,
started_at=datetime(2026, 5, 19, 1, 15, tzinfo=timezone.utc),
)
response = self.client.get(reverse("dashboard"))
@@ -70,6 +78,12 @@ class ViewTests(TestCase):
self.assertContains(response, "warning")
self.assertContains(response, "manual")
self.assertContains(response, "scheduled")
self.assertContains(response, "Backup activity")
self.assertContains(response, "Snapshot health")
self.assertContains(response, "queued 1")
self.assertContains(response, "running 1")
self.assertContains(response, "warning 1")
self.assertContains(response, "failed 1")
def test_dashboard_renders_backup_trend_summary(self) -> None:
self.client.force_login(self.staff_user)

View File

@@ -9,7 +9,7 @@ from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.conf import settings
from django.http import FileResponse, Http404
from django.db.models import Count
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.http import require_POST
@@ -45,7 +45,14 @@ def dashboard(request):
global_config = GlobalConfig.objects.filter(name="default").first()
hosts = list(
HostConfig.objects.select_related("schedule")
.annotate(snapshot_count=Count("snapshots", distinct=True), run_count=Count("runs", distinct=True))
.annotate(
snapshot_count=Count("snapshots", distinct=True),
run_count=Count("runs", distinct=True),
queued_run_count=Count("runs", filter=Q(runs__status=BackupRun.Status.QUEUED), distinct=True),
running_run_count=Count("runs", filter=Q(runs__status=BackupRun.Status.RUNNING), distinct=True),
warning_run_count=Count("runs", filter=Q(runs__status=BackupRun.Status.WARNING), distinct=True),
failed_run_count=Count("runs", filter=Q(runs__status=BackupRun.Status.FAILED), distinct=True),
)
.order_by("host")
)
for host_config in hosts: