diff --git a/src/pobsync_backend/templates/pobsync_backend/dashboard.html b/src/pobsync_backend/templates/pobsync_backend/dashboard.html
index cefa769..89e893d 100644
--- a/src/pobsync_backend/templates/pobsync_backend/dashboard.html
+++ b/src/pobsync_backend/templates/pobsync_backend/dashboard.html
@@ -32,136 +32,14 @@
{% endif %}
-
-
- Required Action
- {% if action_items %}
-
- {% elif counts.hosts %}
- ok No queued, running, unreviewed warning/failed runs, or retention warnings.
- {% else %}
- Add a host to start tracking backup status here.
- {% endif %}
- {% if counts.running_runs or counts.queued_runs %}
-
- {% endif %}
-
-
-
- Next Scheduled Work View all
- {% if next_schedule_rows %}
-
- {% else %}
- No enabled schedules yet.
- {% endif %}
-
-
-
-
- {% if recent_runs %}
-
- {% else %}
- No backup runs recorded yet.
- {% endif %}
-
-
-
- Storage Pressure
- {% if stats_summary.runs_sampled %}
-
-
-
Backup root used
-
- {% if stats_summary.capacity.used_percent is not None %}
- {{ stats_summary.capacity.used_percent|floatformat:1 }}%
- {% else %}
- unknown
- {% endif %}
-
- {% if stats_summary.capacity.used_percent is not None %}
-
-
-
- {% endif %}
-
-
-
- Runway
-
- {% if stats_summary.estimated_days_until_full %}
- {{ stats_summary.estimated_days_until_full }} days
- {% elif stats_summary.estimated_runs_until_full %}
- {{ stats_summary.estimated_runs_until_full }} runs
- {% else %}
- unknown
- {% endif %}
-
-
-
- New data
- {{ stats_summary.avg_daily_literal_data_bytes|filesizeformat }}/day
-
-
- Available
- {{ stats_summary.capacity.available_bytes|filesizeformat }}
-
-
-
- {% else %}
- Storage pressure appears after the first completed backup with stats.
- {% endif %}
-
-
+
+ {% include "pobsync_backend/partials/dashboard_priority.html" %}
+
-
- Hosts
-
- {% for host in hosts %}
-
-
-
-
-
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 %}
-
-
-
-
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 %}
-
-
-
-
-
-
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 }}
-
-
-
-
- {% if host.retention_warning.has_warning %}
-
- retention
- {% if host.retention_warning.prune_exceeded %}
- Scheduled prune would delete {{ host.retention_warning.delete_count }} snapshot(s), above max {{ host.retention_warning.max_delete }}.
- {% endif %}
- {% if host.retention_warning.incomplete_count %}
- {{ host.retention_warning.incomplete_count }} incomplete snapshot(s) need review.
-
- {% endif %}
- {% if host.retention_warning.error %}
- {{ host.retention_warning.error }}
- {% endif %}
-
- {% endif %}
-
- {% empty %}
-
No hosts configured yet.
- {% endfor %}
-
-
+
+ {% include "pobsync_backend/partials/dashboard_hosts.html" %}
+
{% endblock %}
diff --git a/src/pobsync_backend/templates/pobsync_backend/partials/dashboard_hosts.html b/src/pobsync_backend/templates/pobsync_backend/partials/dashboard_hosts.html
new file mode 100644
index 0000000..f255a96
--- /dev/null
+++ b/src/pobsync_backend/templates/pobsync_backend/partials/dashboard_hosts.html
@@ -0,0 +1,123 @@
+
+ Hosts
+
+ {% for host in hosts %}
+
+
+
+
+
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 %}
+
+
+
+
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 %}
+
+
+
+
+
+
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 }}
+
+
+
+
+ {% if host.retention_warning.has_warning %}
+
+ retention
+ {% if host.retention_warning.prune_exceeded %}
+ Scheduled prune would delete {{ host.retention_warning.delete_count }} snapshot(s), above max {{ host.retention_warning.max_delete }}.
+ {% endif %}
+ {% if host.retention_warning.incomplete_count %}
+ {{ host.retention_warning.incomplete_count }} incomplete snapshot(s) need review.
+
+ {% endif %}
+ {% if host.retention_warning.error %}
+ {{ host.retention_warning.error }}
+ {% endif %}
+
+ {% endif %}
+
+ {% empty %}
+
No hosts configured yet.
+ {% endfor %}
+
+
diff --git a/src/pobsync_backend/templates/pobsync_backend/partials/dashboard_priority.html b/src/pobsync_backend/templates/pobsync_backend/partials/dashboard_priority.html
new file mode 100644
index 0000000..d17d01e
--- /dev/null
+++ b/src/pobsync_backend/templates/pobsync_backend/partials/dashboard_priority.html
@@ -0,0 +1,130 @@
+
+
+ Required Action
+ {% if action_items %}
+
+ {% elif counts.hosts %}
+ ok No queued, running, unreviewed warning/failed runs, or retention warnings.
+ {% else %}
+ Add a host to start tracking backup status here.
+ {% endif %}
+ {% if counts.running_runs or counts.queued_runs %}
+
+ {% endif %}
+
+
+
+ Next Scheduled Work View all
+ {% if next_schedule_rows %}
+
+ {% else %}
+ No enabled schedules yet.
+ {% endif %}
+
+
+
+
+ {% if recent_runs %}
+
+ {% else %}
+ No backup runs recorded yet.
+ {% endif %}
+
+
+
+ Storage Pressure
+ {% if stats_summary.runs_sampled %}
+
+
+
Backup root used
+
+ {% if stats_summary.capacity.used_percent is not None %}
+ {{ stats_summary.capacity.used_percent|floatformat:1 }}%
+ {% else %}
+ unknown
+ {% endif %}
+
+ {% if stats_summary.capacity.used_percent is not None %}
+
+
+
+ {% endif %}
+
+
+
+ Runway
+
+ {% if stats_summary.estimated_days_until_full %}
+ {{ stats_summary.estimated_days_until_full }} days
+ {% elif stats_summary.estimated_runs_until_full %}
+ {{ stats_summary.estimated_runs_until_full }} runs
+ {% else %}
+ unknown
+ {% endif %}
+
+
+
+ New data
+ {{ stats_summary.avg_daily_literal_data_bytes|filesizeformat }}/day
+
+
+ Available
+ {{ stats_summary.capacity.available_bytes|filesizeformat }}
+
+
+
+ {% else %}
+ Storage pressure appears after the first completed backup with stats.
+ {% endif %}
+
+
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py
index 451ebe4..951fec8 100644
--- a/src/pobsync_backend/tests/test_views.py
+++ b/src/pobsync_backend/tests/test_views.py
@@ -161,6 +161,8 @@ class ViewTests(TestCase):
self.assertContains(response, "1 backup run waiting.")
self.assertContains(response, "Next Scheduled Work")
self.assertContains(response, "Recent Activity")
+ self.assertContains(response, f'data-refresh-url="{reverse("dashboard_priority_live")}"', html=False)
+ self.assertContains(response, f'data-refresh-url="{reverse("dashboard_hosts_live")}"', html=False)
self.assertContains(response, f'href="{reverse("runs_list")}"', html=False)
self.assertContains(response, f'href="{reverse("runs_list")}?status=queued"', html=False)
self.assertContains(response, f'href="{reverse("runs_list")}?status=running"', html=False)
@@ -174,6 +176,32 @@ class ViewTests(TestCase):
self.assertContains(response, f'href="{reverse("snapshots_list")}"', html=False)
self.assertContains(response, f'href="{reverse("schedules_list")}"', html=False)
+ def test_dashboard_priority_live_returns_status_partial(self) -> None:
+ self.client.force_login(self.staff_user)
+ host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
+ BackupRun.objects.create(host=host, status=BackupRun.Status.RUNNING)
+
+ response = self.client.get(reverse("dashboard_priority_live"))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "Required Action")
+ self.assertContains(response, "Recent Activity")
+ self.assertContains(response, "running")
+ self.assertNotContains(response, " None:
+ self.client.force_login(self.staff_user)
+ host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
+ BackupRun.objects.create(host=host, status=BackupRun.Status.QUEUED)
+
+ response = self.client.get(reverse("dashboard_hosts_live"))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "web-01")
+ self.assertContains(response, "queued 1")
+ self.assertContains(response, "Snapshot health")
+ self.assertNotContains(response, " None:
self.client.force_login(self.staff_user)
GlobalConfig.objects.create(name="default", backup_root="/missing-backup-root")
diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py
index 7c679d9..75352ed 100644
--- a/src/pobsync_backend/views.py
+++ b/src/pobsync_backend/views.py
@@ -48,6 +48,20 @@ from .stats_summary import collect_dashboard_stats, collect_host_stats
@staff_member_required
def dashboard(request):
+ return render(request, "pobsync_backend/dashboard.html", _dashboard_context())
+
+
+@staff_member_required
+def dashboard_priority_live(request):
+ return render(request, "pobsync_backend/partials/dashboard_priority.html", _dashboard_context())
+
+
+@staff_member_required
+def dashboard_hosts_live(request):
+ return render(request, "pobsync_backend/partials/dashboard_hosts.html", _dashboard_context())
+
+
+def _dashboard_context() -> dict[str, object]:
global_config = GlobalConfig.objects.filter(name="default").first()
hosts = list(
HostConfig.objects.select_related("schedule")
@@ -109,7 +123,7 @@ def dashboard(request):
).count(),
},
}
- return render(request, "pobsync_backend/dashboard.html", context)
+ return context
def _dashboard_action_items(hosts: list[HostConfig]) -> list[dict[str, object]]:
diff --git a/src/pobsync_server/urls.py b/src/pobsync_server/urls.py
index e272799..9e71b59 100644
--- a/src/pobsync_server/urls.py
+++ b/src/pobsync_server/urls.py
@@ -8,6 +8,8 @@ from pobsync_backend import api, views
urlpatterns = [
path("", views.dashboard, name="dashboard"),
+ path("dashboard/priority-live/", views.dashboard_priority_live, name="dashboard_priority_live"),
+ path("dashboard/hosts-live/", views.dashboard_hosts_live, name="dashboard_hosts_live"),
path("changelog/", views.changelog, name="changelog"),
path("self-check/", views.self_check, name="self_check"),
path("logs/", views.logs, name="logs"),