issue-26-host-detail-control-page #34

Merged
parkel merged 4 commits from issue-26-host-detail-control-page into master 2026-05-21 14:05:51 +02:00
3 changed files with 129 additions and 50 deletions
Showing only changes of commit ab5291b8d3 - Show all commits

View File

@@ -310,6 +310,53 @@
display: grid;
gap: 8px;
}
.record-list {
display: grid;
gap: 10px;
}
.record-card {
border: 1px solid var(--border);
border-radius: var(--radius);
display: grid;
gap: 10px;
padding: 12px;
}
.record-card-header {
align-items: start;
display: flex;
gap: 12px;
justify-content: space-between;
}
.record-title {
display: grid;
gap: 3px;
min-width: 0;
}
.record-title a {
font-weight: 750;
overflow-wrap: anywhere;
}
.record-facts {
display: grid;
gap: 8px 16px;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.record-fact {
display: grid;
gap: 2px;
min-width: 0;
}
.record-fact .label {
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.record-fact strong,
.record-fact span {
overflow-wrap: anywhere;
}
.action-row,
.activity-row,
.schedule-row {

View File

@@ -406,58 +406,88 @@
</section>
<section class="panel">
<h2>Latest Runs</h2>
<table>
<thead>
<tr>
<th>Status</th>
<th>Started</th>
<th>Ended</th>
<th>Snapshot</th>
<th>Base</th>
</tr>
</thead>
<tbody>
<h2>Latest Runs <a class="button-link secondary compact" href="{% url 'runs_list' %}?host={{ host.host }}">View all</a></h2>
<div class="record-list">
{% for run in latest_runs %}
<tr>
<td><a href="{% url 'run_detail' run.id %}"><span class="status {{ run.status }}">{{ run.status }}</span></a></td>
<td>{{ run.started_at|default:"" }}</td>
<td>{{ run.ended_at|default:"" }}</td>
<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>
<td>{{ run.base_path|default:"" }}</td>
</tr>
<article class="record-card">
<div class="record-card-header">
<div class="record-title">
<a href="{% url 'run_detail' run.id %}">Run {{ run.id }}</a>
<span class="muted">{{ run.run_type }}{% if run.result.duration_seconds %} · {{ run.result.duration_seconds }}s{% endif %}</span>
</div>
<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 %}
<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 %}
</tbody>
</table>
</div>
</section>
<section class="panel">
<h2>Snapshots</h2>
<table>
<thead>
<tr>
<th>Kind</th>
<th>Status</th>
<th>Started</th>
<th>Dirname</th>
<th>Base</th>
</tr>
</thead>
<tbody>
<h2>Snapshots <a class="button-link secondary compact" href="{% url 'snapshots_list' %}?host={{ host.host }}">View all</a></h2>
<div class="record-list">
{% for snapshot in snapshots %}
<tr>
<td>{{ snapshot.kind }}</td>
<td>{{ snapshot.status }}</td>
<td>{{ snapshot.started_at|default:"" }}</td>
<td><a href="{% url 'snapshot_detail' snapshot.id %}">{{ snapshot.dirname }}</a></td>
<td>{% if snapshot.base %}<a href="{% url 'snapshot_detail' snapshot.base.id %}">{{ snapshot.base.dirname }}</a>{% else %}{{ snapshot.base_dirname }}{% endif %}</td>
</tr>
<article class="record-card">
<div class="record-card-header">
<div class="record-title">
<a href="{% url 'snapshot_detail' snapshot.id %}">{{ snapshot.dirname }}</a>
<span class="muted">{{ snapshot.kind }}</span>
</div>
<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 %}
<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 %}
</tbody>
</table>
</div>
</section>
{% endblock %}

View File

@@ -940,6 +940,8 @@ class ViewTests(TestCase):
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]))
self.assertContains(response, f'{reverse("runs_list")}?host={host.host}', html=False)
self.assertContains(response, f'{reverse("snapshots_list")}?host={host.host}', html=False)
def test_host_detail_renders_effective_config_preview(self) -> None:
self.client.force_login(self.staff_user)