|
|
|
@@ -9,70 +9,8 @@
|
|
|
|
<h1>{{ host.host }}</h1>
|
|
|
|
<h1>{{ host.host }}</h1>
|
|
|
|
<div class="page-subtitle">{{ host.address }} · {{ host.enabled|yesno:"enabled,disabled" }}</div>
|
|
|
|
<div class="page-subtitle">{{ host.address }} · {{ host.enabled|yesno:"enabled,disabled" }}</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<section class="actions" aria-label="Host actions">
|
|
|
|
|
|
|
|
<a class="button-link" href="{% url 'edit_host_config' host.host %}">Edit config</a>
|
|
|
|
|
|
|
|
<form method="post" action="{% url 'discover_host_snapshots' host.host %}">
|
|
|
|
|
|
|
|
{% csrf_token %}
|
|
|
|
|
|
|
|
<button type="submit">Discover snapshots</button>
|
|
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
<a class="button-link" href="{% url 'host_retention_plan' host.host %}">Plan retention</a>
|
|
|
|
|
|
|
|
<a class="button-link" href="{% url 'edit_host_schedule' host.host %}">Edit schedule</a>
|
|
|
|
|
|
|
|
<form method="post" action="{% url 'prepare_host_directories' host.host %}">
|
|
|
|
|
|
|
|
{% csrf_token %}
|
|
|
|
|
|
|
|
<button type="submit" class="secondary">Prepare directories</button>
|
|
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
<form method="post" action="{% url 'scan_host_known_key' host.host %}">
|
|
|
|
|
|
|
|
{% csrf_token %}
|
|
|
|
|
|
|
|
<button type="submit" class="secondary">Scan SSH host key</button>
|
|
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
<form method="post" action="{% url 'run_host_preflight' host.host %}">
|
|
|
|
|
|
|
|
{% csrf_token %}
|
|
|
|
|
|
|
|
<button type="submit" class="secondary">Run connection preflight</button>
|
|
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
</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 +39,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><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>
|
|
|
|
<div>
|
|
|
|
<strong>Retention:</strong>
|
|
|
|
{% if host.enabled %}
|
|
|
|
d{{ effective_config.retention.daily }}
|
|
|
|
<span class="status ok">enabled</span>
|
|
|
|
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 %}
|
|
|
|
{% endif %}
|
|
|
|
<div><strong>Excludes:</strong> {{ effective_config.excludes|length }}</div>
|
|
|
|
<span class="muted">{{ host.address }}</span>
|
|
|
|
{% if effective_config.excludes %}
|
|
|
|
</div>
|
|
|
|
<pre>{{ effective_config.excludes|join:" " }}</pre>
|
|
|
|
{% 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 %}
|
|
|
|
{% else %}
|
|
|
|
<div class="muted">No exclude rules configured.</div>
|
|
|
|
<span class="status-summary success">
|
|
|
|
|
|
|
|
<span class="status ok">ok</span>
|
|
|
|
|
|
|
|
<strong>No active blockers for this host.</strong>
|
|
|
|
|
|
|
|
</span>
|
|
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="host-control-meta">
|
|
|
|
|
|
|
|
<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>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</article>
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
</div>
|
|
|
|
{% elif not has_global_config %}
|
|
|
|
|
|
|
|
<span class="status failed">missing global config</span>
|
|
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
</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 schedule</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>
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
</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 %}
|
|
|
|
@@ -214,104 +229,197 @@
|
|
|
|
<div class="metric"><div class="label">Failed</div><div class="value">{{ host_check_summary.failed }}</div></div>
|
|
|
|
<div class="metric"><div class="label">Failed</div><div class="value">{{ host_check_summary.failed }}</div></div>
|
|
|
|
<div class="metric"><div class="label">Skipped</div><div class="value">{{ host_check_summary.skipped }}</div></div>
|
|
|
|
<div class="metric"><div class="label">Skipped</div><div class="value">{{ host_check_summary.skipped }}</div></div>
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
<table>
|
|
|
|
<div class="record-list">
|
|
|
|
<thead>
|
|
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
|
|
|
<th>Check</th>
|
|
|
|
|
|
|
|
<th>Message</th>
|
|
|
|
|
|
|
|
<th>Detail</th>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
|
|
{% for check in host_checks %}
|
|
|
|
{% for check in host_checks %}
|
|
|
|
<tr>
|
|
|
|
<article class="record-card">
|
|
|
|
<td><span class="status {{ check.status }}">{{ check.status }}</span></td>
|
|
|
|
<div class="record-card-header">
|
|
|
|
<td>{{ check.name }}</td>
|
|
|
|
<div class="record-title">
|
|
|
|
<td>{{ check.message }}</td>
|
|
|
|
<strong>{{ check.name }}</strong>
|
|
|
|
<td class="muted">{{ check.detail }}</td>
|
|
|
|
<span class="muted">{{ check.message }}</span>
|
|
|
|
</tr>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<span class="status {{ check.status }}">{{ check.status }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{% if check.detail %}
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Detail</span>
|
|
|
|
|
|
|
|
<span class="muted">{{ check.detail }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
</article>
|
|
|
|
{% endfor %}
|
|
|
|
{% endfor %}
|
|
|
|
</tbody>
|
|
|
|
</div>
|
|
|
|
</table>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel-grid">
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
|
|
|
|
<h2>Configuration</h2>
|
|
|
|
|
|
|
|
<div class="host-control-meta">
|
|
|
|
|
|
|
|
<div><span class="label">Address</span><strong>{{ host.address }}</strong></div>
|
|
|
|
|
|
|
|
<div><span class="label">SSH key</span><strong>{{ host.ssh_credential|default:"global default" }}</strong></div>
|
|
|
|
|
|
|
|
<div><span class="label">SSH</span><strong>{{ host.ssh_user|default:"global" }}{% if host.ssh_port %}:{{ host.ssh_port }}{% endif %}</strong></div>
|
|
|
|
|
|
|
|
<div><span class="label">Backup source</span><strong>{{ host.source_root|default:"global default" }}</strong></div>
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="actions inline">
|
|
|
|
|
|
|
|
<a class="button-link secondary compact" href="{% url 'edit_host_config' host.host %}">Edit config</a>
|
|
|
|
|
|
|
|
<a class="button-link secondary compact" href="{% url 'host_retention_plan' host.host %}">Plan retention</a>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
|
|
|
|
<h2>Connection Preflight & SSH</h2>
|
|
|
|
{% if last_preflight %}
|
|
|
|
{% if last_preflight %}
|
|
|
|
<section class="panel">
|
|
|
|
<div class="host-control-meta">
|
|
|
|
<h2>Connection Preflight</h2>
|
|
|
|
<div>
|
|
|
|
<div class="stack spaced">
|
|
|
|
<span class="label">Preflight</span>
|
|
|
|
<div><strong>Status:</strong> <span class="status {% if last_preflight.ok %}ok{% else %}failed{% endif %}">{% if last_preflight.ok %}ok{% else %}failed{% endif %}</span></div>
|
|
|
|
<strong>
|
|
|
|
<div><strong>Target:</strong> {{ last_preflight.target }}</div>
|
|
|
|
<span class="status {% if last_preflight.ok %}ok{% else %}failed{% endif %}">
|
|
|
|
<div><strong>Backup source:</strong> {{ last_preflight.source_root }}</div>
|
|
|
|
{% if last_preflight.ok %}ok{% else %}failed{% endif %}
|
|
|
|
<div><strong>Remote rsync:</strong> {{ last_preflight.rsync_binary }}</div>
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</strong>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<table>
|
|
|
|
<div><span class="label">Target</span><strong>{{ last_preflight.target }}</strong></div>
|
|
|
|
<thead>
|
|
|
|
<div><span class="label">Backup source</span><strong>{{ last_preflight.source_root }}</strong></div>
|
|
|
|
<tr>
|
|
|
|
<div><span class="label">Remote rsync</span><strong>{{ last_preflight.rsync_binary }}</strong></div>
|
|
|
|
<th>Status</th>
|
|
|
|
</div>
|
|
|
|
<th>Check</th>
|
|
|
|
{% else %}
|
|
|
|
<th>Message</th>
|
|
|
|
<p class="muted">No connection preflight recorded yet.</p>
|
|
|
|
<th>Detail</th>
|
|
|
|
{% endif %}
|
|
|
|
</tr>
|
|
|
|
<div class="actions inline">
|
|
|
|
</thead>
|
|
|
|
<form method="post" action="{% url 'run_host_preflight' host.host %}">
|
|
|
|
<tbody>
|
|
|
|
{% csrf_token %}
|
|
|
|
|
|
|
|
<button type="submit" class="secondary compact">Run connection preflight</button>
|
|
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
<form method="post" action="{% url 'scan_host_known_key' host.host %}">
|
|
|
|
|
|
|
|
{% csrf_token %}
|
|
|
|
|
|
|
|
<button type="submit" class="secondary compact">Scan SSH host key</button>
|
|
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{% if last_preflight.checks %}
|
|
|
|
|
|
|
|
<div class="activity-list">
|
|
|
|
{% for check in last_preflight.checks %}
|
|
|
|
{% for check in last_preflight.checks %}
|
|
|
|
<tr>
|
|
|
|
<div class="activity-row">
|
|
|
|
<td><span class="status {% if check.ok %}ok{% else %}failed{% endif %}">{% if check.ok %}ok{% else %}failed{% endif %}</span></td>
|
|
|
|
<span class="status {% if check.ok %}ok{% else %}failed{% endif %}">
|
|
|
|
<td>{{ check.name }}</td>
|
|
|
|
{% if check.ok %}ok{% else %}failed{% endif %}
|
|
|
|
<td>{{ check.message }}</td>
|
|
|
|
</span>
|
|
|
|
<td class="muted">{{ check.detail }}</td>
|
|
|
|
<span>
|
|
|
|
</tr>
|
|
|
|
<strong>{{ check.name }}</strong>
|
|
|
|
|
|
|
|
<span class="muted">{{ check.message }}{% if check.detail %} · {{ check.detail }}{% endif %}</span>
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
{% endfor %}
|
|
|
|
{% endfor %}
|
|
|
|
</tbody>
|
|
|
|
</div>
|
|
|
|
</table>
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
|
|
|
|
<h2>Snapshot Storage</h2>
|
|
|
|
|
|
|
|
<div class="host-control-meta">
|
|
|
|
|
|
|
|
<div><span class="label">Backup root</span><strong>{{ discovery.backup_root|default:"" }}</strong></div>
|
|
|
|
|
|
|
|
<div><span class="label">Host root</span><strong>{{ discovery.host_root|default:"" }}</strong></div>
|
|
|
|
|
|
|
|
<div><span class="label">Status</span><strong>{{ discovery.message }}</strong></div>
|
|
|
|
|
|
|
|
{% if discovery.kind_counts %}
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
|
|
<span class="label">On disk</span>
|
|
|
|
|
|
|
|
<strong>
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{% if effective_config %}
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
|
|
|
|
<h2>Effective Config</h2>
|
|
|
|
|
|
|
|
<p class="muted">Runtime settings after global defaults and host overrides are combined.</p>
|
|
|
|
|
|
|
|
<div class="record-list">
|
|
|
|
|
|
|
|
<article class="record-card">
|
|
|
|
|
|
|
|
<div class="record-card-header">
|
|
|
|
|
|
|
|
<div class="record-title">
|
|
|
|
|
|
|
|
<strong>Backup target</strong>
|
|
|
|
|
|
|
|
<span class="muted">Source and destination used by rsync.</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-facts">
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">Backup source:</span><strong>{{ effective_config.source_root }}</strong></div>
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">Destination subdir:</span><strong>{{ effective_config.destination_subdir|default:"none" }}</strong></div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
<article class="record-card">
|
|
|
|
|
|
|
|
<div class="record-card-header">
|
|
|
|
|
|
|
|
<div class="record-title">
|
|
|
|
|
|
|
|
<strong>Connection</strong>
|
|
|
|
|
|
|
|
<span class="muted">SSH and rsync execution settings.</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-facts">
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">SSH:</span><strong>{{ effective_config.ssh.user }}@{{ host.address }}:{{ effective_config.ssh.port }}</strong></div>
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">SSH key:</span><strong>{{ effective_config.ssh.credential|default:"none selected" }}</strong></div>
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">SSH options:</span><span>{{ effective_config.ssh.options|join:" " }}</span></div>
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">Rsync binary:</span><strong>{{ effective_config.rsync.binary }}</strong></div>
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">Rsync args:</span><span>{{ effective_config.rsync.args|join:" " }}</span></div>
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">Timeout:</span><strong>{{ effective_config.rsync.timeout_seconds }}s</strong></div>
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">Bandwidth limit:</span><strong>{{ effective_config.rsync.bwlimit_kbps }} KB/s</strong></div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
<article class="record-card">
|
|
|
|
|
|
|
|
<div class="record-card-header">
|
|
|
|
|
|
|
|
<div class="record-title">
|
|
|
|
|
|
|
|
<strong>Selection & retention</strong>
|
|
|
|
|
|
|
|
<span class="muted">Include/exclude rules and retention counts.</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-facts">
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Retention:</span>
|
|
|
|
|
|
|
|
<strong>
|
|
|
|
|
|
|
|
d{{ effective_config.retention.daily }}
|
|
|
|
|
|
|
|
w{{ effective_config.retention.weekly }}
|
|
|
|
|
|
|
|
m{{ effective_config.retention.monthly }}
|
|
|
|
|
|
|
|
y{{ effective_config.retention.yearly }}
|
|
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">Includes:</span><strong>{{ effective_config.includes|length }}</strong></div>
|
|
|
|
|
|
|
|
<div class="record-fact"><span class="label">Excludes:</span><strong>{{ effective_config.excludes|length }}</strong></div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="two-col">
|
|
|
|
|
|
|
|
<div class="stack">
|
|
|
|
|
|
|
|
{% if effective_config.includes %}
|
|
|
|
|
|
|
|
<pre>{{ effective_config.includes|join:" " }}</pre>
|
|
|
|
|
|
|
|
{% else %}
|
|
|
|
|
|
|
|
<div class="muted">No include rules configured.</div>
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="stack">
|
|
|
|
|
|
|
|
{% if effective_config.excludes %}
|
|
|
|
|
|
|
|
<pre>{{ effective_config.excludes|join:" " }}</pre>
|
|
|
|
|
|
|
|
{% else %}
|
|
|
|
|
|
|
|
<div class="muted">No exclude rules configured.</div>
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
<section class="panel">
|
|
|
|
<h2>Backup Control</h2>
|
|
|
|
<h2>Backup Options</h2>
|
|
|
|
<div class="operator-state">
|
|
|
|
<p class="muted">Use this when the quick actions above need a custom label, include/exclude override, or prune limit.</p>
|
|
|
|
{% if active_run %}
|
|
|
|
|
|
|
|
<span class="status {{ active_run.status }}">{{ active_run.status }}</span>
|
|
|
|
|
|
|
|
<a href="{% url 'run_detail' active_run.id %}">Run {{ active_run.id }}</a>
|
|
|
|
|
|
|
|
{% elif has_global_config and host.enabled %}
|
|
|
|
|
|
|
|
<span class="status {{ backup_gate.state }}">{{ backup_gate.state }}</span>
|
|
|
|
|
|
|
|
<span class="muted">{{ backup_gate.message }}</span>
|
|
|
|
|
|
|
|
{% elif not host.enabled %}
|
|
|
|
|
|
|
|
<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 %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<h3>Advanced Options</h3>
|
|
|
|
|
|
|
|
<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 }}
|
|
|
|
@@ -332,58 +440,88 @@
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
<section class="panel">
|
|
|
|
<h2>Latest Runs</h2>
|
|
|
|
<h2>Latest Runs <a class="button-link secondary compact" href="{% url 'runs_list' %}?host={{ host.host }}">View all</a></h2>
|
|
|
|
<table>
|
|
|
|
<div class="record-list">
|
|
|
|
<thead>
|
|
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
|
|
|
<th>Started</th>
|
|
|
|
|
|
|
|
<th>Ended</th>
|
|
|
|
|
|
|
|
<th>Snapshot</th>
|
|
|
|
|
|
|
|
<th>Base</th>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
|
|
{% for run in latest_runs %}
|
|
|
|
{% for run in latest_runs %}
|
|
|
|
<tr>
|
|
|
|
<article class="record-card">
|
|
|
|
<td><a href="{% url 'run_detail' run.id %}"><span class="status {{ run.status }}">{{ run.status }}</span></a></td>
|
|
|
|
<div class="record-card-header">
|
|
|
|
<td>{{ run.started_at|default:"" }}</td>
|
|
|
|
<div class="record-title">
|
|
|
|
<td>{{ run.ended_at|default:"" }}</td>
|
|
|
|
<a href="{% url 'run_detail' run.id %}">Run {{ run.id }}</a>
|
|
|
|
<td>{% if run.snapshot %}<a href="{% url 'snapshot_detail' run.snapshot.id %}">{{ run.snapshot.dirname }}</a>{% else %}<span class="muted">{{ run.snapshot_path }}</span>{% endif %}</td>
|
|
|
|
<span class="muted">{{ run.run_type }}{% if run.result.duration_seconds %} · {{ run.result.duration_seconds }}s{% endif %}</span>
|
|
|
|
<td>{{ run.base_path|default:"" }}</td>
|
|
|
|
</div>
|
|
|
|
</tr>
|
|
|
|
<span class="status {{ run.status }}">{{ run.status }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-facts">
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Started</span>
|
|
|
|
|
|
|
|
<strong>{{ run.started_at|default:run.created_at }}</strong>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Ended</span>
|
|
|
|
|
|
|
|
<strong>{{ run.ended_at|default:"running or queued" }}</strong>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Snapshot</span>
|
|
|
|
|
|
|
|
{% if run.snapshot %}
|
|
|
|
|
|
|
|
<strong><a href="{% url 'snapshot_detail' run.snapshot.id %}">{{ run.snapshot.dirname }}</a></strong>
|
|
|
|
|
|
|
|
{% elif run.snapshot_path %}
|
|
|
|
|
|
|
|
<span class="muted">{{ run.snapshot_path }}</span>
|
|
|
|
|
|
|
|
{% else %}
|
|
|
|
|
|
|
|
<span class="muted">none</span>
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Base</span>
|
|
|
|
|
|
|
|
<span class="muted">{{ run.base_path|default:"none" }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</article>
|
|
|
|
{% empty %}
|
|
|
|
{% empty %}
|
|
|
|
<tr><td colspan="5" class="muted">No backup runs recorded for this host.</td></tr>
|
|
|
|
<p class="muted">No backup runs recorded for this host.</p>
|
|
|
|
{% endfor %}
|
|
|
|
{% endfor %}
|
|
|
|
</tbody>
|
|
|
|
</div>
|
|
|
|
</table>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
<section class="panel">
|
|
|
|
<h2>Snapshots</h2>
|
|
|
|
<h2>Snapshots <a class="button-link secondary compact" href="{% url 'snapshots_list' %}?host={{ host.host }}">View all</a></h2>
|
|
|
|
<table>
|
|
|
|
<div class="record-list">
|
|
|
|
<thead>
|
|
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
|
|
<th>Kind</th>
|
|
|
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
|
|
|
<th>Started</th>
|
|
|
|
|
|
|
|
<th>Dirname</th>
|
|
|
|
|
|
|
|
<th>Base</th>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
|
|
{% for snapshot in snapshots %}
|
|
|
|
{% for snapshot in snapshots %}
|
|
|
|
<tr>
|
|
|
|
<article class="record-card">
|
|
|
|
<td>{{ snapshot.kind }}</td>
|
|
|
|
<div class="record-card-header">
|
|
|
|
<td>{{ snapshot.status }}</td>
|
|
|
|
<div class="record-title">
|
|
|
|
<td>{{ snapshot.started_at|default:"" }}</td>
|
|
|
|
<a href="{% url 'snapshot_detail' snapshot.id %}">{{ snapshot.dirname }}</a>
|
|
|
|
<td><a href="{% url 'snapshot_detail' snapshot.id %}">{{ snapshot.dirname }}</a></td>
|
|
|
|
<span class="muted">{{ snapshot.kind }}</span>
|
|
|
|
<td>{% if snapshot.base %}<a href="{% url 'snapshot_detail' snapshot.base.id %}">{{ snapshot.base.dirname }}</a>{% else %}{{ snapshot.base_dirname }}{% endif %}</td>
|
|
|
|
</div>
|
|
|
|
</tr>
|
|
|
|
<span class="status {{ snapshot.status }}">{{ snapshot.status }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-facts">
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Started</span>
|
|
|
|
|
|
|
|
<strong>{{ snapshot.started_at|default:"unknown" }}</strong>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Ended</span>
|
|
|
|
|
|
|
|
<strong>{{ snapshot.ended_at|default:"unknown" }}</strong>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Base</span>
|
|
|
|
|
|
|
|
{% if snapshot.base %}
|
|
|
|
|
|
|
|
<strong><a href="{% url 'snapshot_detail' snapshot.base.id %}">{{ snapshot.base.dirname }}</a></strong>
|
|
|
|
|
|
|
|
{% elif snapshot.base_dirname %}
|
|
|
|
|
|
|
|
<span class="muted">{{ snapshot.base_dirname }}</span>
|
|
|
|
|
|
|
|
{% else %}
|
|
|
|
|
|
|
|
<span class="muted">none</span>
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="record-fact">
|
|
|
|
|
|
|
|
<span class="label">Path</span>
|
|
|
|
|
|
|
|
<span class="muted">{{ snapshot.path|default:"not recorded" }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</article>
|
|
|
|
{% empty %}
|
|
|
|
{% empty %}
|
|
|
|
<tr><td colspan="5" class="muted">No snapshots discovered for this host.</td></tr>
|
|
|
|
<p class="muted">No snapshots discovered for this host.</p>
|
|
|
|
{% endfor %}
|
|
|
|
{% endfor %}
|
|
|
|
</tbody>
|
|
|
|
</div>
|
|
|
|
</table>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
{% endblock %}
|
|
|
|
{% endblock %}
|
|
|
|
|