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: