From ad2cc5585eca5cef4c350c0d8eee603f3ab4f60c Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 21 May 2026 11:37:25 +0200 Subject: [PATCH] (ui) Add consistent page headers to key views Introduce a shared page-header pattern with kicker, title, subtitle, and actions, then apply it to the dashboard, host detail, run detail, snapshot detail, and retention plan pages. Scope the global app header styles to avoid leaking sticky navigation styles onto page-level headers, and add view assertions for the new page context. Refs #28 --- .../templates/pobsync_backend/base.html | 38 +++++++++++++- .../templates/pobsync_backend/dashboard.html | 17 ++++--- .../pobsync_backend/host_detail.html | 51 ++++++++++--------- .../pobsync_backend/retention_plan.html | 23 +++++---- .../templates/pobsync_backend/run_detail.html | 37 ++++++++------ .../pobsync_backend/snapshot_detail.html | 15 ++++-- src/pobsync_backend/tests/test_views.py | 11 +++- 7 files changed, 130 insertions(+), 62 deletions(-) diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index 070bf2b..0d321f5 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -43,7 +43,7 @@ outline: 3px solid rgba(7, 95, 174, 0.24); outline-offset: 2px; } - header { + body > header { background: var(--panel); border-bottom: 1px solid var(--border); box-shadow: var(--shadow-sm); @@ -105,6 +105,35 @@ } p { margin: 0 0 12px; } p:last-child { margin-bottom: 0; } + .page-header { + align-items: end; + display: flex; + gap: 18px; + justify-content: space-between; + margin-bottom: 20px; + } + .page-title { + display: grid; + gap: 5px; + min-width: 0; + } + .page-title h1 { margin-bottom: 0; overflow-wrap: anywhere; } + .page-kicker { + color: var(--muted); + font-size: 12px; + font-weight: 750; + letter-spacing: 0.05em; + text-transform: uppercase; + } + .page-subtitle { + color: var(--muted); + max-width: 760px; + } + .page-header .actions { + flex: 0 0 auto; + justify-content: flex-end; + margin-bottom: 0; + } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); @@ -483,7 +512,7 @@ padding: 0; } @media (max-width: 800px) { - header { padding: 0 14px; position: static; } + body > header { padding: 0 14px; position: static; } main { padding: 18px 14px 32px; } nav { align-items: flex-start; @@ -493,6 +522,11 @@ } nav strong { flex-basis: 100%; margin-right: 0; } nav .spacer { display: none; } + .page-header { + align-items: stretch; + display: grid; + } + .page-header .actions { justify-content: flex-start; } .two-col { grid-template-columns: 1fr; } .host-card-header { display: grid; } .host-card-status { justify-content: flex-start; max-width: none; } diff --git a/src/pobsync_backend/templates/pobsync_backend/dashboard.html b/src/pobsync_backend/templates/pobsync_backend/dashboard.html index b6067fb..358fd72 100644 --- a/src/pobsync_backend/templates/pobsync_backend/dashboard.html +++ b/src/pobsync_backend/templates/pobsync_backend/dashboard.html @@ -3,12 +3,17 @@ {% block title %}pobsync dashboard{% endblock %} {% block content %} -

Dashboard

- -
- New host - {% if global_config %}Edit global config{% else %}Create global config{% endif %} -
+ {% if not global_config or not counts.hosts %}
diff --git a/src/pobsync_backend/templates/pobsync_backend/host_detail.html b/src/pobsync_backend/templates/pobsync_backend/host_detail.html index ae530e2..60bbae8 100644 --- a/src/pobsync_backend/templates/pobsync_backend/host_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/host_detail.html @@ -3,29 +3,34 @@ {% block title %}{{ host.host }} | pobsync{% endblock %} {% block content %} -

{{ host.host }}

- -
- Edit config -
- {% csrf_token %} - -
- Plan retention - Edit schedule -
- {% csrf_token %} - -
-
- {% csrf_token %} - -
-
- {% csrf_token %} - -
-
+
Snapshots
{{ counts.snapshots }}
diff --git a/src/pobsync_backend/templates/pobsync_backend/retention_plan.html b/src/pobsync_backend/templates/pobsync_backend/retention_plan.html index ba88c9c..c1547b8 100644 --- a/src/pobsync_backend/templates/pobsync_backend/retention_plan.html +++ b/src/pobsync_backend/templates/pobsync_backend/retention_plan.html @@ -3,15 +3,20 @@ {% block title %}Retention plan | {{ host.host }}{% endblock %} {% block content %} -

Retention Plan: {{ host.host }}

- -
- Back to host - Scheduled - Manual - All - Protect bases -
+
Kind
{{ plan.kind }}
diff --git a/src/pobsync_backend/templates/pobsync_backend/run_detail.html b/src/pobsync_backend/templates/pobsync_backend/run_detail.html index 4fbf6aa..8f042f9 100644 --- a/src/pobsync_backend/templates/pobsync_backend/run_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/run_detail.html @@ -3,25 +3,30 @@ {% block title %}Run {{ run.id }} | {{ run.host.host }}{% endblock %} {% block content %} -

Run {{ run.id }}

- -
- Back to host - {% if can_cancel %} -
- {% csrf_token %} - -
- {% endif %} - {% if run.status == "failed" or run.status == "warning" %} - {% if not run.reviewed_at %} -
+
+
Host
{{ run.host.host }}
diff --git a/src/pobsync_backend/templates/pobsync_backend/snapshot_detail.html b/src/pobsync_backend/templates/pobsync_backend/snapshot_detail.html index c37b5ee..9520853 100644 --- a/src/pobsync_backend/templates/pobsync_backend/snapshot_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/snapshot_detail.html @@ -3,11 +3,16 @@ {% block title %}Snapshot {{ snapshot.dirname }} | {{ snapshot.host.host }}{% endblock %} {% block content %} -

{{ snapshot.dirname }}

- -
- Back to host -
+
Host
{{ snapshot.host.host }}
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index a731ba5..cb42255 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -101,6 +101,8 @@ class ViewTests(TestCase): response = self.client.get(reverse("dashboard")) self.assertEqual(response.status_code, 200) + self.assertContains(response, "Control panel") + self.assertContains(response, "Backup health, required action, storage pressure, and recent activity in one place.") self.assertContains(response, "Dashboard") self.assertContains(response, "web-01") self.assertContains(response, "20260519-021500Z__ABCDEFGH") @@ -869,6 +871,8 @@ class ViewTests(TestCase): response = self.client.get(reverse("host_detail", args=[host.host])) self.assertEqual(response.status_code, 200) + self.assertContains(response, "Host") + self.assertContains(response, "web-01.example.test") self.assertContains(response, "Effective Config") self.assertContains(response, "Backup source:") self.assertNotContains(response, "Source root:") @@ -1425,6 +1429,8 @@ class ViewTests(TestCase): response = self.client.get(reverse("run_detail", args=[run.id])) self.assertEqual(response.status_code, 200) + self.assertContains(response, "Backup run") + self.assertContains(response, "web-01") self.assertContains(response, "Failure") self.assertContains(response, "transport") self.assertContains(response, "Check network connectivity.") @@ -1615,6 +1621,7 @@ class ViewTests(TestCase): response = self.client.get(reverse("snapshot_detail", args=[base.id])) self.assertEqual(response.status_code, 200) + self.assertContains(response, "Snapshot") self.assertContains(response, base.dirname) self.assertContains(response, "BASESNAP") self.assertContains(response, "Stats") @@ -1697,7 +1704,9 @@ class ViewTests(TestCase): response = self.client.get(reverse("host_retention_plan", args=[host.host])) self.assertEqual(response.status_code, 200) - self.assertContains(response, "Retention Plan: web-01") + self.assertContains(response, "Retention") + self.assertContains(response, "Preview which snapshots stay") + self.assertContains(response, "web-01") self.assertContains(response, old_snapshot.dirname) self.assertContains(response, new_snapshot.dirname) self.assertContains(response, "newest")