diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index beef593..31b3ecd 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -370,6 +370,43 @@ gap: 10px; justify-content: space-between; padding-top: 8px; + } + .host-control-grid { + display: grid; + gap: 14px; + grid-template-columns: minmax(280px, 1.25fr) repeat(3, minmax(220px, 1fr)); + margin-bottom: 20px; + } + .host-control-panel { + display: grid; + gap: 12px; + margin-bottom: 0; + } + .host-control-panel > h2:first-child { margin-bottom: 0; } + .host-control-primary { + display: grid; + gap: 8px; + } + .host-control-meta { + display: grid; + gap: 6px; + } + .host-control-meta > div { + align-items: baseline; + border-top: 1px solid var(--border); + display: flex; + gap: 10px; + justify-content: space-between; + padding-top: 7px; + } + .host-control-meta .label { + color: var(--muted); + font-size: 12px; + font-weight: 650; + text-transform: uppercase; + } + .host-control-meta strong { + text-align: right; } .status-summary { align-items: center; @@ -641,6 +678,7 @@ .page-header .actions { justify-content: flex-start; } .two-col { grid-template-columns: 1fr; } .dashboard-priority-grid { grid-template-columns: 1fr; } + .host-control-grid { grid-template-columns: 1fr; } .schedule-row { grid-template-columns: 1fr; } .schedule-time { justify-items: start; text-align: left; } .host-card-header { display: grid; } diff --git a/src/pobsync_backend/templates/pobsync_backend/host_detail.html b/src/pobsync_backend/templates/pobsync_backend/host_detail.html index 60bbae8..899762b 100644 --- a/src/pobsync_backend/templates/pobsync_backend/host_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/host_detail.html @@ -32,47 +32,6 @@ -
-
Snapshots
{{ counts.snapshots }}
-
Runs
{{ counts.runs }}
-
Queued
{{ counts.queued_runs }}
-
Running
{{ counts.running_runs }}
-
Failed Runs
{{ counts.failed_runs }}
-
Incomplete
{{ counts.incomplete_snapshots }}
-
- -
-
-

Config

-
-
Address: {{ host.address }}
-
Enabled: {{ host.enabled|yesno:"yes,no" }}
-
SSH key: {{ host.ssh_credential|default:"global default" }}
-
SSH: {{ host.ssh_user|default:"global" }}{% if host.ssh_port %}:{{ host.ssh_port }}{% endif %}
-
Backup source: {{ host.source_root|default:"global default" }}
-
Retention: daily {{ host.retention_daily }}, weekly {{ host.retention_weekly }}, monthly {{ host.retention_monthly }}, yearly {{ host.retention_yearly }}
-
-
- -
-

Schedule

- {% if schedule %} -
-
Schedule expression: {{ schedule.cron_expr }}
-
Evaluated by the pobsync scheduler service.
-
Enabled: {{ schedule.enabled|yesno:"yes,no" }}
-
Next run: {% if next_run_at %}{{ next_run_at|date:"Y-m-d H:i T" }} {{ scheduler_timezone }}{% endif %}
-
Prune: {{ schedule.prune|yesno:"yes,no" }}
-
Last status: {{ schedule.last_status|default:"" }}
-
Last started: {{ schedule.last_started_at|default:"" }}
-
Last finished: {{ schedule.last_finished_at|default:"" }}
-
- {% else %} -

No schedule configured.

- {% endif %} -
-
- {% if retention_warning.has_warning %}

Retention Warnings

@@ -101,60 +60,137 @@
{% endif %} - {% if effective_config %} -
-

Effective Config

-
-
-
Backup source: {{ effective_config.source_root }}
-
Destination subdir: {{ effective_config.destination_subdir|default:"none" }}
-
SSH: {{ effective_config.ssh.user }}@{{ host.address }}:{{ effective_config.ssh.port }}
-
SSH key: {{ effective_config.ssh.credential|default:"none selected" }}
-
SSH options: {{ effective_config.ssh.options|join:" " }}
-
Rsync binary: {{ effective_config.rsync.binary }}
-
Rsync args: {{ effective_config.rsync.args|join:" " }}
-
Timeout: {{ effective_config.rsync.timeout_seconds }}s
-
Bandwidth limit: {{ effective_config.rsync.bwlimit_kbps }} KB/s
-
- Retention: - d{{ effective_config.retention.daily }} - w{{ effective_config.retention.weekly }} - m{{ effective_config.retention.monthly }} - y{{ effective_config.retention.yearly }} -
-
-
-
Includes: {{ effective_config.includes|length }}
- {% if effective_config.includes %} -
{{ effective_config.includes|join:"
" }}
+
+
+

Host Status

+
+
+ {% if host.enabled %} + enabled {% else %} -
No include rules configured.
- {% endif %} -
Excludes: {{ effective_config.excludes|length }}
- {% if effective_config.excludes %} -
{{ effective_config.excludes|join:"
" }}
- {% else %} -
No exclude rules configured.
+ disabled {% endif %} + {{ host.address }}
+ {% if active_run %} + + {{ active_run.status }} + Run {{ active_run.id }} is active. + + {% elif counts.failed_runs %} + + failed + {{ counts.failed_runs }} failed run{{ counts.failed_runs|pluralize }} need review. + + {% elif retention_warning.has_warning %} + + warning + Retention needs attention. + + {% else %} + + ok + No active blockers for this host. + + {% endif %}
-
- {% endif %} +
+
Snapshots{{ counts.snapshots }}
+
Runs{{ counts.runs }}
+
Incomplete{{ counts.incomplete_snapshots }}
+
+ -
-

Snapshot Discovery

-
-
Backup root: {{ discovery.backup_root|default:"" }}
-
Host root: {{ discovery.host_root|default:"" }}
-
Status: {{ discovery.message }}
- {% if discovery.kind_counts %} -
On disk: - scheduled {{ discovery.kind_counts.scheduled|default:0 }}, - manual {{ discovery.kind_counts.manual|default:0 }}, - incomplete {{ discovery.kind_counts.incomplete|default:0 }} +
+

Backup Control

+
+ {% if active_run %} + {{ active_run.status }} + Run {{ active_run.id }} + {% elif has_global_config and host.enabled %} + {{ backup_gate.state }} + {{ backup_gate.message }} + {% elif not host.enabled %} + disabled + {% elif not has_global_config %} + missing global config + {% endif %} +
+
+
+ {% csrf_token %} + + + + +
+
+ {% csrf_token %} + + +
+
+ {% if active_run %} +

Wait for the active run to finish, or cancel it from the run detail page.

+ {% elif not can_queue_dry_run or not can_queue_real_backup %} + {% if not has_global_config %} +

Create the default global config before queueing backups.

+ {% elif not host.enabled %} +

Enable this host before queueing backups.

+ {% elif backup_gate.real_blockers %} +

Real backups are blocked by failed preflight checks. Dry-runs may still be available when storage-only checks fail.

+ {% endif %} + {% endif %} +
+ +
+

Schedule Edit

+ {% if schedule %} +
+
Schedule expression{{ schedule.cron_expr }}
+
Next run{% if next_run_at %}{{ next_run_at|date:"Y-m-d H:i T" }}{% else %}none{% endif %}
+
Timezone{{ scheduler_timezone }}
+
Prune{{ schedule.prune|yesno:"yes,no" }}
+
Last status{{ schedule.last_status|default:"none" }}
+
+

Evaluated by the pobsync scheduler service.

+ {% else %} +

No schedule configured.

+ Add schedule + {% endif %} +
+ +
+

Current Activity

+ {% if latest_runs %} + {% with run=latest_runs.0 %} + + {{ run.status }} + + Run {{ run.id }} + {{ run.run_type }} ยท {{ run.started_at|default:run.created_at }} + + + {% endwith %} + {% else %} +

No backup runs recorded for this host.

+ {% endif %} + {% if stats_summary.latest_run.duration_seconds is not None %} +
+
Latest duration{{ stats_summary.latest_run.duration_seconds }}s
+
New data{{ stats_summary.latest_run.rsync.literal_data_bytes|filesizeformat }}
{% endif %} -
+ +
+ +
+
Snapshots
{{ counts.snapshots }}
+
Runs
{{ counts.runs }}
+
Queued
{{ counts.queued_runs }}
+
Running
{{ counts.running_runs }}
+
Failed Runs
{{ counts.failed_runs }}
+
Incomplete
{{ counts.incomplete_snapshots }}
{% if stats_summary.runs %} @@ -268,50 +304,94 @@
{% endif %} -
-

Backup Control

-
- {% if active_run %} - {{ active_run.status }} - Run {{ active_run.id }} - {% elif has_global_config and host.enabled %} - {{ backup_gate.state }} - {{ backup_gate.message }} - {% elif not host.enabled %} - disabled - {% elif not has_global_config %} - missing global config - {% endif %} -
- -
-
- {% csrf_token %} - - - - -
-
- {% csrf_token %} - - -
+
+
+

Configuration

+
+
Address{{ host.address }}
+
SSH key{{ host.ssh_credential|default:"global default" }}
+
SSH{{ host.ssh_user|default:"global" }}{% if host.ssh_port %}:{{ host.ssh_port }}{% endif %}
+
Backup source{{ host.source_root|default:"global default" }}
+
Retentiond{{ host.retention_daily }} w{{ host.retention_weekly }} m{{ host.retention_monthly }} y{{ host.retention_yearly }}
+
+
- {% if active_run %} -

Wait for the active run to finish, or cancel it from the run detail page.

- {% elif not can_queue_dry_run or not can_queue_real_backup %} - {% if not has_global_config %} -

Create the default global config before queueing backups.

- {% elif not host.enabled %} -

Enable this host before queueing backups.

- {% elif backup_gate.real_blockers %} -

Real backups are blocked by failed preflight checks. Dry-runs may still be available when storage-only checks fail.

- {% endif %} - {% endif %} +
+

Snapshot Storage

+
+
Backup root{{ discovery.backup_root|default:"" }}
+
Host root{{ discovery.host_root|default:"" }}
+
Status{{ discovery.message }}
+ {% if discovery.kind_counts %} +
+ On disk + + scheduled {{ discovery.kind_counts.scheduled|default:0 }}, + manual {{ discovery.kind_counts.manual|default:0 }}, + incomplete {{ discovery.kind_counts.incomplete|default:0 }} + +
+ {% endif %} +
+
+
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
+
+
-

Advanced Options

+ {% if effective_config %} +
+

Effective Config

+
+
+
Backup source: {{ effective_config.source_root }}
+
Destination subdir: {{ effective_config.destination_subdir|default:"none" }}
+
SSH: {{ effective_config.ssh.user }}@{{ host.address }}:{{ effective_config.ssh.port }}
+
SSH key: {{ effective_config.ssh.credential|default:"none selected" }}
+
SSH options: {{ effective_config.ssh.options|join:" " }}
+
Rsync binary: {{ effective_config.rsync.binary }}
+
Rsync args: {{ effective_config.rsync.args|join:" " }}
+
Timeout: {{ effective_config.rsync.timeout_seconds }}s
+
Bandwidth limit: {{ effective_config.rsync.bwlimit_kbps }} KB/s
+
+ Retention: + d{{ effective_config.retention.daily }} + w{{ effective_config.retention.weekly }} + m{{ effective_config.retention.monthly }} + y{{ effective_config.retention.yearly }} +
+
+
+
Includes: {{ effective_config.includes|length }}
+ {% if effective_config.includes %} +
{{ effective_config.includes|join:"
" }}
+ {% else %} +
No include rules configured.
+ {% endif %} +
Excludes: {{ effective_config.excludes|length }}
+ {% if effective_config.excludes %} +
{{ effective_config.excludes|join:"
" }}
+ {% else %} +
No exclude rules configured.
+ {% endif %} +
+
+
+ {% endif %} + +
+

Backup Options

+

Use this when the quick actions above need a custom label, include/exclude override, or prune limit.

{% csrf_token %} {{ manual_backup_form.non_field_errors }} diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index e026558..c5d73ae 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -923,7 +923,7 @@ class ViewTests(TestCase): self.assertContains(response, "15 2 * * *") self.assertContains(response, "Schedule expression") self.assertContains(response, "Evaluated by the pobsync scheduler service.") - self.assertContains(response, "Next run:") + self.assertContains(response, "Next run") self.assertContains(response, "UTC") self.assertContains(response, "20260519-021500Z__ABCDEFGH") self.assertContains(response, "Discover snapshots") @@ -936,7 +936,7 @@ class ViewTests(TestCase): self.assertContains(response, "Host Check") self.assertContains(response, reverse("prepare_host_directories", args=[host.host])) self.assertContains(response, "warning") - self.assertContains(response, "Snapshot Discovery") + self.assertContains(response, "Snapshot Storage") self.assertContains(response, reverse("queue_manual_backup", args=[host.host])) self.assertContains(response, reverse("run_detail", args=[BackupRun.objects.get().id])) self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id])) @@ -1221,7 +1221,8 @@ class ViewTests(TestCase): response = self.client.get(reverse("host_detail", args=[host.host])) self.assertEqual(response.status_code, 200) - self.assertContains(response, f"Host root: {backup_root / host.host}") + self.assertContains(response, "Host root") + self.assertContains(response, str(backup_root / host.host)) self.assertContains(response, "Found 2 snapshot directories") self.assertContains(response, "scheduled 1") self.assertContains(response, "incomplete 1")