(ui) Promote host detail operator controls
Add a first-screen host control workspace with status, backup actions, schedule state, and current activity so the host detail page behaves as the primary operator page instead of starting with raw configuration blocks. Refs #26
This commit is contained in:
@@ -370,6 +370,43 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding-top: 8px;
|
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 {
|
.status-summary {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -641,6 +678,7 @@
|
|||||||
.page-header .actions { justify-content: flex-start; }
|
.page-header .actions { justify-content: flex-start; }
|
||||||
.two-col { grid-template-columns: 1fr; }
|
.two-col { grid-template-columns: 1fr; }
|
||||||
.dashboard-priority-grid { 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-row { grid-template-columns: 1fr; }
|
||||||
.schedule-time { justify-items: start; text-align: left; }
|
.schedule-time { justify-items: start; text-align: left; }
|
||||||
.host-card-header { display: grid; }
|
.host-card-header { display: grid; }
|
||||||
|
|||||||
@@ -32,47 +32,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="grid" aria-label="Host summary">
|
|
||||||
<div class="metric"><div class="label">Snapshots</div><div class="value">{{ counts.snapshots }}</div></div>
|
|
||||||
<div class="metric"><div class="label">Runs</div><div class="value">{{ counts.runs }}</div></div>
|
|
||||||
<div class="metric"><div class="label">Queued</div><div class="value">{{ counts.queued_runs }}</div></div>
|
|
||||||
<div class="metric"><div class="label">Running</div><div class="value">{{ counts.running_runs }}</div></div>
|
|
||||||
<div class="metric"><div class="label">Failed Runs</div><div class="value">{{ counts.failed_runs }}</div></div>
|
|
||||||
<div class="metric"><div class="label">Incomplete</div><div class="value">{{ counts.incomplete_snapshots }}</div></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="two-col">
|
|
||||||
<section class="panel">
|
|
||||||
<h2>Config</h2>
|
|
||||||
<div class="stack">
|
|
||||||
<div><strong>Address:</strong> {{ host.address }}</div>
|
|
||||||
<div><strong>Enabled:</strong> {{ host.enabled|yesno:"yes,no" }}</div>
|
|
||||||
<div><strong>SSH key:</strong> {{ host.ssh_credential|default:"global default" }}</div>
|
|
||||||
<div><strong>SSH:</strong> {{ host.ssh_user|default:"global" }}{% if host.ssh_port %}:{{ host.ssh_port }}{% endif %}</div>
|
|
||||||
<div><strong>Backup source:</strong> {{ host.source_root|default:"global default" }}</div>
|
|
||||||
<div><strong>Retention:</strong> daily {{ host.retention_daily }}, weekly {{ host.retention_weekly }}, monthly {{ host.retention_monthly }}, yearly {{ host.retention_yearly }}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2>Schedule</h2>
|
|
||||||
{% if schedule %}
|
|
||||||
<div class="stack">
|
|
||||||
<div><strong>Schedule expression:</strong> {{ schedule.cron_expr }}</div>
|
|
||||||
<div class="muted">Evaluated by the pobsync scheduler service.</div>
|
|
||||||
<div><strong>Enabled:</strong> {{ schedule.enabled|yesno:"yes,no" }}</div>
|
|
||||||
<div><strong>Next run:</strong> {% if next_run_at %}{{ next_run_at|date:"Y-m-d H:i T" }} <span class="muted">{{ scheduler_timezone }}</span>{% endif %}</div>
|
|
||||||
<div><strong>Prune:</strong> {{ schedule.prune|yesno:"yes,no" }}</div>
|
|
||||||
<div><strong>Last status:</strong> {{ schedule.last_status|default:"" }}</div>
|
|
||||||
<div><strong>Last started:</strong> {{ schedule.last_started_at|default:"" }}</div>
|
|
||||||
<div><strong>Last finished:</strong> {{ schedule.last_finished_at|default:"" }}</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="muted">No schedule configured.</p>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if retention_warning.has_warning %}
|
{% if retention_warning.has_warning %}
|
||||||
<section class="panel highlight warning">
|
<section class="panel highlight warning">
|
||||||
<h2>Retention Warnings</h2>
|
<h2>Retention Warnings</h2>
|
||||||
@@ -101,60 +60,137 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if effective_config %}
|
<section class="host-control-grid" aria-label="Host control workspace">
|
||||||
<section class="panel">
|
<article class="panel host-control-panel">
|
||||||
<h2>Effective Config</h2>
|
<h2>Host Status</h2>
|
||||||
<div class="two-col">
|
<div class="host-control-primary">
|
||||||
<div class="stack">
|
<div>
|
||||||
<div><strong>Backup source:</strong> {{ effective_config.source_root }}</div>
|
{% if host.enabled %}
|
||||||
<div><strong>Destination subdir:</strong> {{ effective_config.destination_subdir|default:"none" }}</div>
|
<span class="status ok">enabled</span>
|
||||||
<div><strong>SSH:</strong> {{ effective_config.ssh.user }}@{{ host.address }}:{{ effective_config.ssh.port }}</div>
|
|
||||||
<div><strong>SSH key:</strong> {{ effective_config.ssh.credential|default:"none selected" }}</div>
|
|
||||||
<div><strong>SSH options:</strong> {{ effective_config.ssh.options|join:" " }}</div>
|
|
||||||
<div><strong>Rsync binary:</strong> {{ effective_config.rsync.binary }}</div>
|
|
||||||
<div><strong>Rsync args:</strong> {{ effective_config.rsync.args|join:" " }}</div>
|
|
||||||
<div><strong>Timeout:</strong> {{ effective_config.rsync.timeout_seconds }}s</div>
|
|
||||||
<div><strong>Bandwidth limit:</strong> {{ effective_config.rsync.bwlimit_kbps }} KB/s</div>
|
|
||||||
<div>
|
|
||||||
<strong>Retention:</strong>
|
|
||||||
d{{ effective_config.retention.daily }}
|
|
||||||
w{{ effective_config.retention.weekly }}
|
|
||||||
m{{ effective_config.retention.monthly }}
|
|
||||||
y{{ effective_config.retention.yearly }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stack">
|
|
||||||
<div><strong>Includes:</strong> {{ effective_config.includes|length }}</div>
|
|
||||||
{% if effective_config.includes %}
|
|
||||||
<pre>{{ effective_config.includes|join:" " }}</pre>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="muted">No include rules configured.</div>
|
<span class="status failed">disabled</span>
|
||||||
{% endif %}
|
|
||||||
<div><strong>Excludes:</strong> {{ effective_config.excludes|length }}</div>
|
|
||||||
{% if effective_config.excludes %}
|
|
||||||
<pre>{{ effective_config.excludes|join:" " }}</pre>
|
|
||||||
{% else %}
|
|
||||||
<div class="muted">No exclude rules configured.</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<span class="muted">{{ host.address }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% if active_run %}
|
||||||
|
<a class="status-summary {{ active_run.status }}" href="{% url 'run_detail' active_run.id %}">
|
||||||
|
<span class="status {{ active_run.status }}">{{ active_run.status }}</span>
|
||||||
|
<strong>Run {{ active_run.id }} is active.</strong>
|
||||||
|
</a>
|
||||||
|
{% elif counts.failed_runs %}
|
||||||
|
<a class="status-summary failed" href="{% url 'runs_list' %}?host={{ host.host }}&status=failed&review=needed">
|
||||||
|
<span class="status failed">failed</span>
|
||||||
|
<strong>{{ counts.failed_runs }} failed run{{ counts.failed_runs|pluralize }} need review.</strong>
|
||||||
|
</a>
|
||||||
|
{% elif retention_warning.has_warning %}
|
||||||
|
<span class="status-summary warning">
|
||||||
|
<span class="status warning">warning</span>
|
||||||
|
<strong>Retention needs attention.</strong>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-summary success">
|
||||||
|
<span class="status ok">ok</span>
|
||||||
|
<strong>No active blockers for this host.</strong>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div class="host-control-meta">
|
||||||
{% endif %}
|
<div><span class="label">Snapshots</span><strong>{{ counts.snapshots }}</strong></div>
|
||||||
|
<div><span class="label">Runs</span><strong>{{ counts.runs }}</strong></div>
|
||||||
|
<div><span class="label">Incomplete</span><strong>{{ counts.incomplete_snapshots }}</strong></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
<section class="panel">
|
<article class="panel host-control-panel">
|
||||||
<h2>Snapshot Discovery</h2>
|
<h2>Backup Control</h2>
|
||||||
<div class="stack">
|
<div class="operator-state">
|
||||||
<div><strong>Backup root:</strong> {{ discovery.backup_root|default:"" }}</div>
|
{% if active_run %}
|
||||||
<div><strong>Host root:</strong> {{ discovery.host_root|default:"" }}</div>
|
<span class="status {{ active_run.status }}">{{ active_run.status }}</span>
|
||||||
<div><strong>Status:</strong> {{ discovery.message }}</div>
|
<a href="{% url 'run_detail' active_run.id %}">Run {{ active_run.id }}</a>
|
||||||
{% if discovery.kind_counts %}
|
{% elif has_global_config and host.enabled %}
|
||||||
<div><strong>On disk:</strong>
|
<span class="status {{ backup_gate.state }}">{{ backup_gate.state }}</span>
|
||||||
scheduled {{ discovery.kind_counts.scheduled|default:0 }},
|
<span class="muted">{{ backup_gate.message }}</span>
|
||||||
manual {{ discovery.kind_counts.manual|default:0 }},
|
{% elif not host.enabled %}
|
||||||
incomplete {{ discovery.kind_counts.incomplete|default:0 }}
|
<span class="status failed">disabled</span>
|
||||||
|
{% elif not has_global_config %}
|
||||||
|
<span class="status failed">missing global config</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<section class="actions inline" aria-label="Quick backup actions">
|
||||||
|
<form class="inline-form" method="post" action="{% url 'queue_manual_backup' host.host %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="dry_run" value="on">
|
||||||
|
<input type="hidden" name="verbose_output" value="on">
|
||||||
|
<input type="hidden" name="prune_max_delete" value="10">
|
||||||
|
<button type="submit" class="secondary" {% if not can_queue_dry_run %}disabled{% endif %}>Queue dry-run</button>
|
||||||
|
</form>
|
||||||
|
<form class="inline-form" method="post" action="{% url 'queue_manual_backup' host.host %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="prune_max_delete" value="10">
|
||||||
|
<button type="submit" {% if not can_queue_real_backup %}disabled{% endif %}>Queue backup</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% if active_run %}
|
||||||
|
<p class="muted">Wait for the active run to finish, or cancel it from the run detail page.</p>
|
||||||
|
{% elif not can_queue_dry_run or not can_queue_real_backup %}
|
||||||
|
{% if not has_global_config %}
|
||||||
|
<p class="muted">Create the default global config before queueing backups.</p>
|
||||||
|
{% elif not host.enabled %}
|
||||||
|
<p class="muted">Enable this host before queueing backups.</p>
|
||||||
|
{% elif backup_gate.real_blockers %}
|
||||||
|
<p class="muted">Real backups are blocked by failed preflight checks. Dry-runs may still be available when storage-only checks fail.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="panel host-control-panel">
|
||||||
|
<h2>Schedule <a class="button-link secondary compact" href="{% url 'edit_host_schedule' host.host %}">Edit</a></h2>
|
||||||
|
{% if schedule %}
|
||||||
|
<div class="host-control-meta">
|
||||||
|
<div><span class="label">Schedule expression</span><strong>{{ schedule.cron_expr }}</strong></div>
|
||||||
|
<div><span class="label">Next run</span><strong>{% if next_run_at %}{{ next_run_at|date:"Y-m-d H:i T" }}{% else %}none{% endif %}</strong></div>
|
||||||
|
<div><span class="label">Timezone</span><strong>{{ scheduler_timezone }}</strong></div>
|
||||||
|
<div><span class="label">Prune</span><strong>{{ schedule.prune|yesno:"yes,no" }}</strong></div>
|
||||||
|
<div><span class="label">Last status</span><strong>{{ schedule.last_status|default:"none" }}</strong></div>
|
||||||
|
</div>
|
||||||
|
<p class="muted">Evaluated by the pobsync scheduler service.</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">No schedule configured.</p>
|
||||||
|
<a class="button-link secondary compact" href="{% url 'edit_host_schedule' host.host %}">Add schedule</a>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="panel host-control-panel">
|
||||||
|
<h2>Current Activity</h2>
|
||||||
|
{% if latest_runs %}
|
||||||
|
{% with run=latest_runs.0 %}
|
||||||
|
<a class="activity-row" href="{% url 'run_detail' run.id %}">
|
||||||
|
<span class="status {{ run.status }}">{{ run.status }}</span>
|
||||||
|
<span>
|
||||||
|
<strong>Run {{ run.id }}</strong>
|
||||||
|
<span class="muted">{{ run.run_type }} · {{ run.started_at|default:run.created_at }}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">No backup runs recorded for this host.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if stats_summary.latest_run.duration_seconds is not None %}
|
||||||
|
<div class="host-control-meta">
|
||||||
|
<div><span class="label">Latest duration</span><strong>{{ stats_summary.latest_run.duration_seconds }}s</strong></div>
|
||||||
|
<div><span class="label">New data</span><strong>{{ stats_summary.latest_run.rsync.literal_data_bytes|filesizeformat }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid" aria-label="Host summary">
|
||||||
|
<div class="metric"><div class="label">Snapshots</div><div class="value">{{ counts.snapshots }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Runs</div><div class="value">{{ counts.runs }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Queued</div><div class="value">{{ counts.queued_runs }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Running</div><div class="value">{{ counts.running_runs }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Failed Runs</div><div class="value">{{ counts.failed_runs }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Incomplete</div><div class="value">{{ counts.incomplete_snapshots }}</div></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if stats_summary.runs %}
|
{% if stats_summary.runs %}
|
||||||
@@ -268,50 +304,94 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<section class="panel">
|
<div class="two-col">
|
||||||
<h2>Backup Control</h2>
|
<section class="panel">
|
||||||
<div class="operator-state">
|
<h2>Configuration</h2>
|
||||||
{% if active_run %}
|
<div class="host-control-meta">
|
||||||
<span class="status {{ active_run.status }}">{{ active_run.status }}</span>
|
<div><span class="label">Address</span><strong>{{ host.address }}</strong></div>
|
||||||
<a href="{% url 'run_detail' active_run.id %}">Run {{ active_run.id }}</a>
|
<div><span class="label">SSH key</span><strong>{{ host.ssh_credential|default:"global default" }}</strong></div>
|
||||||
{% elif has_global_config and host.enabled %}
|
<div><span class="label">SSH</span><strong>{{ host.ssh_user|default:"global" }}{% if host.ssh_port %}:{{ host.ssh_port }}{% endif %}</strong></div>
|
||||||
<span class="status {{ backup_gate.state }}">{{ backup_gate.state }}</span>
|
<div><span class="label">Backup source</span><strong>{{ host.source_root|default:"global default" }}</strong></div>
|
||||||
<span class="muted">{{ backup_gate.message }}</span>
|
<div><span class="label">Retention</span><strong>d{{ host.retention_daily }} w{{ host.retention_weekly }} m{{ host.retention_monthly }} y{{ host.retention_yearly }}</strong></div>
|
||||||
{% elif not host.enabled %}
|
</div>
|
||||||
<span class="status failed">disabled</span>
|
<div class="actions inline">
|
||||||
{% elif not has_global_config %}
|
<a class="button-link secondary compact" href="{% url 'edit_host_config' host.host %}">Edit config</a>
|
||||||
<span class="status failed">missing global config</span>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="actions inline" aria-label="Quick backup actions">
|
|
||||||
<form class="inline-form" method="post" action="{% url 'queue_manual_backup' host.host %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="dry_run" value="on">
|
|
||||||
<input type="hidden" name="verbose_output" value="on">
|
|
||||||
<input type="hidden" name="prune_max_delete" value="10">
|
|
||||||
<button type="submit" class="secondary" {% if not can_queue_dry_run %}disabled{% endif %}>Queue dry-run</button>
|
|
||||||
</form>
|
|
||||||
<form class="inline-form" method="post" action="{% url 'queue_manual_backup' host.host %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="prune_max_delete" value="10">
|
|
||||||
<button type="submit" {% if not can_queue_real_backup %}disabled{% endif %}>Queue backup</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if active_run %}
|
<section class="panel">
|
||||||
<p class="muted">Wait for the active run to finish, or cancel it from the run detail page.</p>
|
<h2>Snapshot Storage</h2>
|
||||||
{% elif not can_queue_dry_run or not can_queue_real_backup %}
|
<div class="host-control-meta">
|
||||||
{% if not has_global_config %}
|
<div><span class="label">Backup root</span><strong>{{ discovery.backup_root|default:"" }}</strong></div>
|
||||||
<p class="muted">Create the default global config before queueing backups.</p>
|
<div><span class="label">Host root</span><strong>{{ discovery.host_root|default:"" }}</strong></div>
|
||||||
{% elif not host.enabled %}
|
<div><span class="label">Status</span><strong>{{ discovery.message }}</strong></div>
|
||||||
<p class="muted">Enable this host before queueing backups.</p>
|
{% if discovery.kind_counts %}
|
||||||
{% elif backup_gate.real_blockers %}
|
<div>
|
||||||
<p class="muted">Real backups are blocked by failed preflight checks. Dry-runs may still be available when storage-only checks fail.</p>
|
<span class="label">On disk</span>
|
||||||
{% endif %}
|
<strong>
|
||||||
{% endif %}
|
scheduled {{ discovery.kind_counts.scheduled|default:0 }},
|
||||||
|
manual {{ discovery.kind_counts.manual|default:0 }},
|
||||||
|
incomplete {{ discovery.kind_counts.incomplete|default:0 }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="actions inline">
|
||||||
|
<form method="post" action="{% url 'discover_host_snapshots' host.host %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="secondary compact">Discover snapshots</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="{% url 'prepare_host_directories' host.host %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="secondary compact">Prepare directories</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Advanced Options</h3>
|
{% if effective_config %}
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Effective Config</h2>
|
||||||
|
<div class="two-col">
|
||||||
|
<div class="stack">
|
||||||
|
<div><strong>Backup source:</strong> {{ effective_config.source_root }}</div>
|
||||||
|
<div><strong>Destination subdir:</strong> {{ effective_config.destination_subdir|default:"none" }}</div>
|
||||||
|
<div><strong>SSH:</strong> {{ effective_config.ssh.user }}@{{ host.address }}:{{ effective_config.ssh.port }}</div>
|
||||||
|
<div><strong>SSH key:</strong> {{ effective_config.ssh.credential|default:"none selected" }}</div>
|
||||||
|
<div><strong>SSH options:</strong> {{ effective_config.ssh.options|join:" " }}</div>
|
||||||
|
<div><strong>Rsync binary:</strong> {{ effective_config.rsync.binary }}</div>
|
||||||
|
<div><strong>Rsync args:</strong> {{ effective_config.rsync.args|join:" " }}</div>
|
||||||
|
<div><strong>Timeout:</strong> {{ effective_config.rsync.timeout_seconds }}s</div>
|
||||||
|
<div><strong>Bandwidth limit:</strong> {{ effective_config.rsync.bwlimit_kbps }} KB/s</div>
|
||||||
|
<div>
|
||||||
|
<strong>Retention:</strong>
|
||||||
|
d{{ effective_config.retention.daily }}
|
||||||
|
w{{ effective_config.retention.weekly }}
|
||||||
|
m{{ effective_config.retention.monthly }}
|
||||||
|
y{{ effective_config.retention.yearly }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stack">
|
||||||
|
<div><strong>Includes:</strong> {{ effective_config.includes|length }}</div>
|
||||||
|
{% if effective_config.includes %}
|
||||||
|
<pre>{{ effective_config.includes|join:" " }}</pre>
|
||||||
|
{% else %}
|
||||||
|
<div class="muted">No include rules configured.</div>
|
||||||
|
{% endif %}
|
||||||
|
<div><strong>Excludes:</strong> {{ effective_config.excludes|length }}</div>
|
||||||
|
{% if effective_config.excludes %}
|
||||||
|
<pre>{{ effective_config.excludes|join:" " }}</pre>
|
||||||
|
{% else %}
|
||||||
|
<div class="muted">No exclude rules configured.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Backup Options</h2>
|
||||||
|
<p class="muted">Use this when the quick actions above need a custom label, include/exclude override, or prune limit.</p>
|
||||||
<form method="post" action="{% url 'queue_manual_backup' host.host %}" class="form-grid">
|
<form method="post" action="{% url 'queue_manual_backup' host.host %}" class="form-grid">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ manual_backup_form.non_field_errors }}
|
{{ manual_backup_form.non_field_errors }}
|
||||||
|
|||||||
@@ -923,7 +923,7 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "15 2 * * *")
|
self.assertContains(response, "15 2 * * *")
|
||||||
self.assertContains(response, "Schedule expression")
|
self.assertContains(response, "Schedule expression")
|
||||||
self.assertContains(response, "Evaluated by the pobsync scheduler service.")
|
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, "UTC")
|
||||||
self.assertContains(response, "20260519-021500Z__ABCDEFGH")
|
self.assertContains(response, "20260519-021500Z__ABCDEFGH")
|
||||||
self.assertContains(response, "Discover snapshots")
|
self.assertContains(response, "Discover snapshots")
|
||||||
@@ -936,7 +936,7 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "Host Check")
|
self.assertContains(response, "Host Check")
|
||||||
self.assertContains(response, reverse("prepare_host_directories", args=[host.host]))
|
self.assertContains(response, reverse("prepare_host_directories", args=[host.host]))
|
||||||
self.assertContains(response, "warning")
|
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("queue_manual_backup", args=[host.host]))
|
||||||
self.assertContains(response, reverse("run_detail", args=[BackupRun.objects.get().id]))
|
self.assertContains(response, reverse("run_detail", args=[BackupRun.objects.get().id]))
|
||||||
self.assertContains(response, reverse("snapshot_detail", args=[snapshot.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]))
|
response = self.client.get(reverse("host_detail", args=[host.host]))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, f"Host root:</strong> {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, "Found 2 snapshot directories")
|
||||||
self.assertContains(response, "scheduled 1")
|
self.assertContains(response, "scheduled 1")
|
||||||
self.assertContains(response, "incomplete 1")
|
self.assertContains(response, "incomplete 1")
|
||||||
|
|||||||
Reference in New Issue
Block a user