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 %} -
- {% for item in action_items %} - - {{ item.label }} - - {{ item.host.host }} - {{ item.message }} - - - {% endfor %} -
- {% 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 %} -
- -
-

Recent Activity View all

- {% 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
{{ counts.enabled_hosts }}/{{ counts.hosts }}
@@ -216,128 +94,13 @@ {% endif %}
-
-

Hosts

-
- {% for host in hosts %} -
-
-
- {{ host.host }} - {{ host.address }} -
-
- {{ 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 %} -
-
-
-
-
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. -
- {% csrf_token %} - -
- {% 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 %} +
+
+
+ {{ host.host }} + {{ host.address }} +
+
+ {{ 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 %} +
+
+
+
+
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. +
+ {% csrf_token %} + +
+ {% 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 %} +
+ {% for item in action_items %} + + {{ item.label }} + + {{ item.host.host }} + {{ item.message }} + + + {% endfor %} +
+ {% 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 %} +
+ +
+

Recent Activity View all

+ {% 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"),