From a0fd33fcb861b35aa18eadc61f073807af56c970 Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 21 May 2026 01:41:45 +0200 Subject: [PATCH] (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. --- .../templates/pobsync_backend/base.html | 36 +++-- .../templates/pobsync_backend/dashboard.html | 132 ++++++++++-------- src/pobsync_backend/tests/test_views.py | 14 ++ src/pobsync_backend/views.py | 11 +- 4 files changed, 125 insertions(+), 68 deletions(-) diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index 7cf0dc8..fdab62d 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -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)); } } diff --git a/src/pobsync_backend/templates/pobsync_backend/dashboard.html b/src/pobsync_backend/templates/pobsync_backend/dashboard.html index 9a6b755..5ff1775 100644 --- a/src/pobsync_backend/templates/pobsync_backend/dashboard.html +++ b/src/pobsync_backend/templates/pobsync_backend/dashboard.html @@ -63,72 +63,90 @@
{{ host.enabled|yesno:"enabled,disabled" }} + {% if host.queued_run_count %} + queued {{ host.queued_run_count }} + {% endif %} + {% if host.running_run_count %} + running {{ host.running_run_count }} + {% endif %} + {% if host.warning_run_count %} + warning {{ host.warning_run_count }} + {% endif %} + {% if host.failed_run_count %} + failed {{ host.failed_run_count }} + {% endif %}
-
-
-
Latest Snapshot
-
- {% if host.latest_snapshot %} - {{ host.latest_snapshot.dirname }} -
{{ host.latest_snapshot.kind }} {{ host.latest_snapshot.status }}
- {% else %} - none - {% endif %} +
+
Backup activity
+
+
+
Latest Snapshot
+
+ {% if host.latest_snapshot %} + {{ host.latest_snapshot.dirname }} +
{{ host.latest_snapshot.kind }} {{ host.latest_snapshot.status }}
+ {% else %} + none + {% endif %} +
-
-
-
Last Good Backup
-
- {% if host.stats_summary.latest_good_run.id %} - Run {{ host.stats_summary.latest_good_run.id }} -
{{ host.stats_summary.latest_good_run.run_type }} {{ host.stats_summary.latest_good_run.duration_seconds|default:"" }}{% if host.stats_summary.latest_good_run.duration_seconds is not None %}s{% endif %}
- {% else %} - none - {% endif %} +
+
Last Good Backup
+
+ {% if host.stats_summary.latest_good_run.id %} + Run {{ host.stats_summary.latest_good_run.id }} +
{{ host.stats_summary.latest_good_run.run_type }} {{ host.stats_summary.latest_good_run.duration_seconds|default:"" }}{% if host.stats_summary.latest_good_run.duration_seconds is not None %}s{% endif %}
+ {% else %} + none + {% endif %} +
-
-
-
Latest Issue
-
- {% if host.stats_summary.latest_problem_run.id %} - Run {{ host.stats_summary.latest_problem_run.id }} -
{{ host.stats_summary.latest_problem_run.status }}
-
{{ host.stats_summary.latest_problem_run.run_type }} {{ host.stats_summary.latest_problem_run.duration_seconds|default:"" }}{% if host.stats_summary.latest_problem_run.duration_seconds is not None %}s{% endif %}
- {% else %} - none - {% endif %} +
+
Latest Issue
+
+ {% if host.stats_summary.latest_problem_run.id %} + Run {{ host.stats_summary.latest_problem_run.id }} +
{{ host.stats_summary.latest_problem_run.status }}
+
{{ host.stats_summary.latest_problem_run.run_type }} {{ host.stats_summary.latest_problem_run.duration_seconds|default:"" }}{% if host.stats_summary.latest_problem_run.duration_seconds is not None %}s{% endif %}
+ {% else %} + none + {% endif %} +
-
-
-
Next Run
-
- {% if host.next_run_at %} - {{ host.next_run_at|date:"Y-m-d H:i T" }} -
{{ scheduler_timezone }}
- {% else %} - none - {% endif %} +
+
Next Run
+
+ {% if host.next_run_at %} + {{ host.next_run_at|date:"Y-m-d H:i T" }} +
{{ scheduler_timezone }}
+ {% else %} + none + {% endif %} +
-
-
-
Snapshots
-
{{ host.snapshot_count }}
-
-
-
Runs
-
{{ host.run_count }}
-
-
-
New Data
-
{{ host.stats_summary.latest_run.rsync.literal_data_bytes|filesizeformat }}
-
-
-
Retention
-
d{{ host.retention_daily }} w{{ host.retention_weekly }} m{{ host.retention_monthly }} y{{ host.retention_yearly }}
+
+
Snapshot health
+
+
+
Snapshots
+
{{ host.snapshot_count }}
+
+
+
Runs
+
{{ host.run_count }}
+
+
+
New Data
+
{{ host.stats_summary.latest_run.rsync.literal_data_bytes|filesizeformat }}
+
+
+
Retention
+
d{{ host.retention_daily }} w{{ host.retention_weekly }} m{{ host.retention_monthly }} y{{ host.retention_yearly }}
+
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index cad4cce..f96e886 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -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) diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index 4a23de8..03a739a 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -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: