diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index f2453d6..7a343fe 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -280,6 +280,17 @@ } button.secondary:hover, .button-link.secondary:hover { background: #eef3f8; } + button.danger, + .button-link.danger { + background: var(--failed); + border-color: var(--failed); + color: #fff; + } + button.danger:hover, + .button-link.danger:hover { + background: #842828; + border-color: #842828; + } button.compact, .button-link.compact { font-size: 12px; @@ -679,6 +690,30 @@ .message.error { border-color: #e8b4b4; background: #fff0f0; color: var(--failed); } .message.warning { border-color: #e7cf8a; background: #fff8df; color: var(--running); } .form-grid { display: grid; gap: 15px; max-width: 720px; } + .filter-form { + align-items: end; + display: grid; + gap: 15px; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + max-width: none; + } + .form-actions { + align-items: center; + border-top: 1px solid var(--border); + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 4px; + padding-top: 15px; + } + .form-actions .button-link.secondary { margin-left: auto; } + .filter-form .form-actions { + border-top: 0; + justify-content: flex-end; + margin-top: 0; + padding-top: 0; + } + .filter-form .form-actions .button-link.secondary { margin-left: 0; } .field { display: grid; gap: 6px; } .field label { font-weight: 700; } .field input[type="text"], .field input[type="number"], .field select, .field textarea { @@ -736,6 +771,7 @@ .host-control-grid { grid-template-columns: 1fr; } .schedule-row { grid-template-columns: 1fr; } .schedule-time { justify-items: start; text-align: left; } + .form-actions .button-link.secondary { margin-left: 0; } .host-card-header { display: grid; } .host-card-status { justify-content: flex-start; max-width: none; } .host-card-layout { grid-template-columns: 1fr; } diff --git a/src/pobsync_backend/templates/pobsync_backend/global_form.html b/src/pobsync_backend/templates/pobsync_backend/global_form.html index 98fe535..41c05bc 100644 --- a/src/pobsync_backend/templates/pobsync_backend/global_form.html +++ b/src/pobsync_backend/templates/pobsync_backend/global_form.html @@ -33,8 +33,9 @@ {% endfor %} -
+
+ Cancel
diff --git a/src/pobsync_backend/templates/pobsync_backend/host_detail.html b/src/pobsync_backend/templates/pobsync_backend/host_detail.html index 316fde3..76b67bb 100644 --- a/src/pobsync_backend/templates/pobsync_backend/host_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/host_detail.html @@ -433,7 +433,7 @@
{% endfor %} -
+
diff --git a/src/pobsync_backend/templates/pobsync_backend/host_form.html b/src/pobsync_backend/templates/pobsync_backend/host_form.html index 6847561..06a8abd 100644 --- a/src/pobsync_backend/templates/pobsync_backend/host_form.html +++ b/src/pobsync_backend/templates/pobsync_backend/host_form.html @@ -33,8 +33,13 @@
{% endfor %} -
+
+ {% if host %} + Cancel + {% else %} + Cancel + {% endif %}
diff --git a/src/pobsync_backend/templates/pobsync_backend/logs.html b/src/pobsync_backend/templates/pobsync_backend/logs.html index 8eb57d2..f917473 100644 --- a/src/pobsync_backend/templates/pobsync_backend/logs.html +++ b/src/pobsync_backend/templates/pobsync_backend/logs.html @@ -16,7 +16,7 @@

Filter

-
+
-
+
+ Clear
diff --git a/src/pobsync_backend/templates/pobsync_backend/purged_snapshots.html b/src/pobsync_backend/templates/pobsync_backend/purged_snapshots.html index d6ed8ce..f623dd7 100644 --- a/src/pobsync_backend/templates/pobsync_backend/purged_snapshots.html +++ b/src/pobsync_backend/templates/pobsync_backend/purged_snapshots.html @@ -16,7 +16,7 @@

Filters

-
+
-
+
Clear
diff --git a/src/pobsync_backend/templates/pobsync_backend/retention_plan.html b/src/pobsync_backend/templates/pobsync_backend/retention_plan.html index c1547b8..02fd800 100644 --- a/src/pobsync_backend/templates/pobsync_backend/retention_plan.html +++ b/src/pobsync_backend/templates/pobsync_backend/retention_plan.html @@ -104,8 +104,12 @@
{% if plan.delete %} -
+

Apply Retention

+

+ This permanently deletes the snapshot directories listed in Would Delete. Confirm the host and delete count + before applying the plan. +

{% csrf_token %} {{ apply_form.non_field_errors }} @@ -138,8 +142,9 @@
{{ apply_form.confirm_delete_count.help_text }}
-
- +
+ + Cancel
@@ -200,6 +205,10 @@

Cleanup Incomplete Snapshots

+

+ This deletes only incomplete snapshot directories and their tracking records. Successful manual and scheduled + snapshots are not touched. +

{% csrf_token %} {{ incomplete_cleanup_form.non_field_errors }} @@ -225,8 +234,9 @@
{{ incomplete_cleanup_form.confirm_delete_count.help_text }}
-
- +
+ + Cancel
diff --git a/src/pobsync_backend/templates/pobsync_backend/run_detail.html b/src/pobsync_backend/templates/pobsync_backend/run_detail.html index 8f042f9..6e06b06 100644 --- a/src/pobsync_backend/templates/pobsync_backend/run_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/run_detail.html @@ -11,20 +11,6 @@
Back to host - {% if can_cancel %} -
- {% csrf_token %} - -
- {% endif %} - {% if run.status == "failed" or run.status == "warning" %} - {% if not run.reviewed_at %} -
- {% csrf_token %} - -
- {% endif %} - {% endif %}
@@ -35,6 +21,22 @@
Rsync
{{ run.rsync_exit_code|default:"" }}
+ {% if can_cancel %} +
+

Run Control

+

+ Cancelling a queued run stops it immediately. Cancelling a running run asks the worker to stop + and records the cancellation request on this run. +

+
+ {% csrf_token %} +
+ +
+
+
+ {% endif %} + {% if failure %}

Failure

@@ -46,6 +48,21 @@
{% endif %} + {% if run.status == "failed" or run.status == "warning" %} + {% if not run.reviewed_at %} +
+

Review Required

+

Mark this run as reviewed after you have checked the failure or warning and no longer need it in the action queue.

+
+ {% csrf_token %} +
+ +
+
+
+ {% endif %} + {% endif %} + {% if run.reviewed_at %}

Review

diff --git a/src/pobsync_backend/templates/pobsync_backend/runs_list.html b/src/pobsync_backend/templates/pobsync_backend/runs_list.html index 9be3a57..f366fb4 100644 --- a/src/pobsync_backend/templates/pobsync_backend/runs_list.html +++ b/src/pobsync_backend/templates/pobsync_backend/runs_list.html @@ -16,7 +16,7 @@

Filters

-
+
-
+
Clear
diff --git a/src/pobsync_backend/templates/pobsync_backend/schedule_form.html b/src/pobsync_backend/templates/pobsync_backend/schedule_form.html index 9aa7581..d9cee47 100644 --- a/src/pobsync_backend/templates/pobsync_backend/schedule_form.html +++ b/src/pobsync_backend/templates/pobsync_backend/schedule_form.html @@ -30,8 +30,9 @@
{% endfor %} -
+
+ Cancel
diff --git a/src/pobsync_backend/templates/pobsync_backend/schedules_list.html b/src/pobsync_backend/templates/pobsync_backend/schedules_list.html index ada648b..9600b22 100644 --- a/src/pobsync_backend/templates/pobsync_backend/schedules_list.html +++ b/src/pobsync_backend/templates/pobsync_backend/schedules_list.html @@ -16,7 +16,7 @@

Filters

-
+
-
+
Clear
diff --git a/src/pobsync_backend/templates/pobsync_backend/snapshots_list.html b/src/pobsync_backend/templates/pobsync_backend/snapshots_list.html index a6d8e7c..7676452 100644 --- a/src/pobsync_backend/templates/pobsync_backend/snapshots_list.html +++ b/src/pobsync_backend/templates/pobsync_backend/snapshots_list.html @@ -16,7 +16,7 @@

Filters

- +
-
+
Clear
diff --git a/src/pobsync_backend/templates/pobsync_backend/ssh_credential_form.html b/src/pobsync_backend/templates/pobsync_backend/ssh_credential_form.html index 092480f..f0bbd6f 100644 --- a/src/pobsync_backend/templates/pobsync_backend/ssh_credential_form.html +++ b/src/pobsync_backend/templates/pobsync_backend/ssh_credential_form.html @@ -41,8 +41,9 @@
{% endfor %} -
+
+ Cancel
@@ -64,7 +65,10 @@
- +
+ + Cancel +
{% endif %} diff --git a/src/pobsync_backend/templates/pobsync_backend/ssh_credential_generate.html b/src/pobsync_backend/templates/pobsync_backend/ssh_credential_generate.html index 1b5933c..acb10ba 100644 --- a/src/pobsync_backend/templates/pobsync_backend/ssh_credential_generate.html +++ b/src/pobsync_backend/templates/pobsync_backend/ssh_credential_generate.html @@ -29,8 +29,9 @@ {% endfor %} -
+
+ Cancel
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 1f63df6..e221eeb 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -233,6 +233,9 @@ class ViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "Runs") self.assertContains(response, "Review queued, running, completed") + self.assertContains(response, "Apply filters") + self.assertContains(response, reverse("runs_list")) + self.assertContains(response, "Clear") self.assertContains(response, f"Run {failed.id}") self.assertContains(response, "web-01") self.assertContains(response, "needed") @@ -275,6 +278,9 @@ class ViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "Snapshots") self.assertContains(response, "Browse discovered scheduled, manual, and incomplete snapshots") + self.assertContains(response, "Apply filters") + self.assertContains(response, reverse("snapshots_list")) + self.assertContains(response, "Clear") self.assertContains(response, manual.dirname) self.assertContains(response, "web-01") self.assertNotContains(response, scheduled.dirname) @@ -291,6 +297,9 @@ class ViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "Schedules") self.assertContains(response, "Review configured backup schedules") + self.assertContains(response, "Apply filters") + self.assertContains(response, reverse("schedules_list")) + self.assertContains(response, "Clear") self.assertContains(response, "web-01") self.assertContains(response, "15 2 * * *") self.assertContains(response, "success") @@ -428,6 +437,9 @@ class ViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "Logs") self.assertContains(response, "Filter pobsync service logs") + self.assertContains(response, "Filter logs") + self.assertContains(response, reverse("logs")) + self.assertContains(response, "Clear") self.assertContains(response, "web-01 failed backup run 12") self.assertNotContains(response, "web-02 failed backup run 12") self.assertNotContains(response, "started") @@ -458,6 +470,9 @@ class ViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "Purged Snapshots") self.assertContains(response, "Audit trail for snapshots removed") + self.assertContains(response, "Apply filters") + self.assertContains(response, reverse("purged_snapshots")) + self.assertContains(response, "Clear") self.assertContains(response, "20260518-021500Z__OLDSNAP") self.assertContains(response, "outside retention policy") self.assertContains(response, "Scheduled") @@ -542,6 +557,21 @@ class ViewTests(TestCase): self.assertEqual(credential.private_key, "UPLOADED PRIVATE KEY\n") self.assertEqual(credential.public_key, "DERIVED PUBLIC KEY") + def test_ssh_credential_forms_render_cancel_actions(self) -> None: + self.client.force_login(self.staff_user) + credential = SshCredential.objects.create(name="backup-key") + + create_response = self.client.get(reverse("create_ssh_credential")) + edit_response = self.client.get(reverse("edit_ssh_credential", args=[credential.id])) + generate_response = self.client.get(reverse("generate_ssh_credential")) + + for response in (create_response, edit_response, generate_response): + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Cancel") + self.assertContains(response, reverse("ssh_credentials")) + self.assertContains(edit_response, "Delete SSH key") + self.assertContains(edit_response, 'class="danger"', html=False) + def test_ssh_credentials_view_generates_filesystem_key(self) -> None: self.client.force_login(self.staff_user) with TemporaryDirectory() as tmp, override_settings(POBSYNC_HOME=str(Path(tmp) / "home")): @@ -746,6 +776,8 @@ class ViewTests(TestCase): self.assertContains(response, f'value="{credential.id}" selected') self.assertContains(response, "--archive") self.assertContains(response, "/proc/***") + self.assertContains(response, "Cancel") + self.assertContains(response, reverse("dashboard")) def test_global_config_form_renders_static_container_backup_root_on_edit(self) -> None: self.client.force_login(self.staff_user) @@ -1635,8 +1667,11 @@ class ViewTests(TestCase): response = self.client.get(reverse("run_detail", args=[run.id])) self.assertEqual(response.status_code, 200) + self.assertContains(response, "Run Control") + self.assertContains(response, "Cancelling a queued run stops it immediately") self.assertContains(response, "Cancel run") self.assertContains(response, reverse("cancel_run", args=[run.id])) + self.assertContains(response, 'class="danger"', html=False) def test_run_detail_renders_worker_execution_metadata(self) -> None: self.client.force_login(self.staff_user) @@ -1820,6 +1855,9 @@ class ViewTests(TestCase): self.assertNotContains(response, "
Source
", html=True) self.assertContains(response, "Confirm delete count") self.assertContains(response, "Type 1 to confirm the current number of planned deletions.") + self.assertContains(response, "This permanently deletes the snapshot directories listed in Would Delete.") + self.assertContains(response, 'class="danger"', html=False) + self.assertContains(response, "Cancel") def test_retention_plan_warns_when_scheduled_prune_limit_is_exceeded(self) -> None: self.client.force_login(self.staff_user) @@ -1895,6 +1933,8 @@ class ViewTests(TestCase): self.assertContains(response, "excluded from retention cleanup") self.assertContains(response, "Delete incomplete snapshots") self.assertContains(response, "Type 1 to confirm the current number of incomplete snapshots.") + self.assertContains(response, "This deletes only incomplete snapshot directories") + self.assertContains(response, 'class="danger"', html=False) def test_incomplete_cleanup_deletes_incomplete_snapshot_after_confirmation(self) -> None: self.client.force_login(self.staff_user) @@ -2142,6 +2182,8 @@ class ViewTests(TestCase): self.assertContains(response, "evaluated by the pobsync scheduler service") self.assertContains(response, "15 2 * * *") self.assertContains(response, "Save schedule") + self.assertContains(response, "Cancel") + self.assertContains(response, reverse("host_detail", args=[host.host])) def test_schedule_form_creates_schedule(self) -> None: self.client.force_login(self.staff_user) @@ -2243,6 +2285,8 @@ class ViewTests(TestCase): self.assertContains(response, "/srv") self.assertContains(response, "*.tmp") self.assertContains(response, "--numeric-ids") + self.assertContains(response, "Cancel") + self.assertContains(response, reverse("host_detail", args=[host.host])) def test_host_config_form_renders_effective_config_check(self) -> None: self.client.force_login(self.staff_user)