issue-27-dashboard-information-architecture #33
@@ -288,7 +288,7 @@
|
||||
.dashboard-priority-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: minmax(280px, 1.3fr) repeat(2, minmax(240px, 1fr));
|
||||
grid-template-columns: minmax(280px, 1.25fr) repeat(3, minmax(220px, 1fr));
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.priority-panel {
|
||||
@@ -342,6 +342,34 @@
|
||||
.schedule-time {
|
||||
justify-items: end;
|
||||
text-align: right;
|
||||
}
|
||||
.storage-priority {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.storage-priority .label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.storage-priority .value {
|
||||
font-size: 27px;
|
||||
font-weight: 760;
|
||||
line-height: 1.15;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.storage-priority-facts {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.storage-priority-facts > div {
|
||||
align-items: baseline;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.status-summary {
|
||||
align-items: center;
|
||||
|
||||
@@ -114,6 +114,53 @@
|
||||
<p class="muted">No backup runs recorded yet.</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
<article class="panel priority-panel">
|
||||
<h2>Storage Pressure</h2>
|
||||
{% if stats_summary.runs_sampled %}
|
||||
<div class="storage-priority">
|
||||
<div>
|
||||
<div class="label">Backup root used</div>
|
||||
<div class="value">
|
||||
{% if stats_summary.capacity.used_percent is not None %}
|
||||
{{ stats_summary.capacity.used_percent|floatformat:1 }}%
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if stats_summary.capacity.used_percent is not None %}
|
||||
<div class="storage-meter" aria-label="Backup root storage usage">
|
||||
<span style="width: {{ stats_summary.capacity.used_percent|floatformat:0 }}%"></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="storage-priority-facts">
|
||||
<div>
|
||||
<span class="label">Runway</span>
|
||||
<strong>
|
||||
{% if stats_summary.estimated_days_until_full %}
|
||||
{{ stats_summary.estimated_days_until_full }} days
|
||||
{% elif stats_summary.estimated_runs_until_full %}
|
||||
{{ stats_summary.estimated_runs_until_full }} runs
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">New data</span>
|
||||
<strong>{{ stats_summary.avg_daily_literal_data_bytes|filesizeformat }}/day</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Available</span>
|
||||
<strong>{{ stats_summary.capacity.available_bytes|filesizeformat }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">Storage pressure appears after the first completed backup with stats.</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="grid" aria-label="Summary">
|
||||
@@ -129,24 +176,6 @@
|
||||
<h2>Backup Trends</h2>
|
||||
{% if stats_summary.runs_sampled %}
|
||||
<div class="insight-grid" aria-label="Backup trends">
|
||||
<div class="insight-main">
|
||||
<div class="label">Storage Used</div>
|
||||
<div class="value">
|
||||
{% if stats_summary.capacity.used_percent is not None %}
|
||||
{{ stats_summary.capacity.used_percent|floatformat:1 }}%
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if stats_summary.capacity.used_percent is not None %}
|
||||
<div class="storage-meter" aria-label="Backup root storage usage">
|
||||
<span style="width: {{ stats_summary.capacity.used_percent|floatformat:0 }}%"></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="muted">
|
||||
{{ stats_summary.capacity.available_bytes|filesizeformat }} available from the backup root.
|
||||
</div>
|
||||
</div>
|
||||
<div class="insight-item">
|
||||
<div class="label">Runway</div>
|
||||
<div class="value">
|
||||
|
||||
@@ -177,14 +177,14 @@ class ViewTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Backup Trends")
|
||||
self.assertContains(response, "Storage Used")
|
||||
self.assertContains(response, "Storage Pressure")
|
||||
self.assertContains(response, "Backup root used")
|
||||
self.assertContains(response, "Runway")
|
||||
self.assertContains(response, "New Data")
|
||||
self.assertContains(response, "Link-Dest Savings")
|
||||
self.assertContains(response, "80.0%")
|
||||
self.assertContains(response, "10 days")
|
||||
self.assertContains(response, "Warnings")
|
||||
self.assertContains(response, "Queued")
|
||||
self.assertContains(response, "Next Run")
|
||||
self.assertContains(response, "UTC")
|
||||
self.assertContains(response, "10")
|
||||
|
||||
@@ -135,7 +135,7 @@ def _dashboard_action_items(hosts: list[HostConfig]) -> list[dict[str, object]]:
|
||||
"url": _runs_list_url(host=host_config.host, status="warning", review="needed"),
|
||||
}
|
||||
)
|
||||
if host_config.retention_warning.has_warning:
|
||||
if host_config.retention_warning.get("has_warning"):
|
||||
action_items.append(
|
||||
{
|
||||
"host": host_config,
|
||||
@@ -168,15 +168,15 @@ def _dashboard_next_schedule_rows() -> list[dict[str, object]]:
|
||||
|
||||
def _retention_warning_summary(retention_warning) -> str:
|
||||
parts = []
|
||||
if retention_warning.prune_exceeded:
|
||||
if retention_warning.get("prune_exceeded"):
|
||||
parts.append(
|
||||
f"Scheduled prune would delete {retention_warning.delete_count} snapshot(s), "
|
||||
f"above max {retention_warning.max_delete}."
|
||||
f"Scheduled prune would delete {retention_warning.get('delete_count')} snapshot(s), "
|
||||
f"above max {retention_warning.get('max_delete')}."
|
||||
)
|
||||
if retention_warning.incomplete_count:
|
||||
parts.append(f"{retention_warning.incomplete_count} incomplete snapshot(s) need review.")
|
||||
if retention_warning.error:
|
||||
parts.append(str(retention_warning.error))
|
||||
if retention_warning.get("incomplete_count"):
|
||||
parts.append(f"{retention_warning.get('incomplete_count')} incomplete snapshot(s) need review.")
|
||||
if retention_warning.get("error"):
|
||||
parts.append(str(retention_warning.get("error")))
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user