3 Commits

Author SHA1 Message Date
0e2f48ab65 (ui) Remove dashboard panel overflow traps
Let dashboard-specific panels size naturally instead of inheriting generic
panel overflow behavior, and tighten activity and host card sizing so long
content wraps without creating internal scrollbars.

Refs #38
2026-05-21 14:50:05 +02:00
b55950e24a (ui) Add dashboard section responsive hooks
Give dashboard summary, trends, and host sections dedicated layout hooks
and tighten their responsive behavior so metrics and host cards remain
readable on narrower screens.

Refs #38
2026-05-21 14:46:23 +02:00
025cd0336c (ui) Harden dashboard responsive layout
Rework the dashboard priority grid to avoid cramped four-column layouts,
prevent stretched empty panels, and make long host and snapshot text wrap
safely across dashboard cards.

Refs #38
2026-05-21 14:43:24 +02:00
3 changed files with 105 additions and 16 deletions

View File

@@ -179,10 +179,23 @@
box-shadow: var(--shadow);
transform: translateY(-1px);
}
.metric-link:focus-visible {
outline: 3px solid #93c5fd;
outline-offset: 2px;
}
.metric-link:focus-visible {
outline: 3px solid #93c5fd;
outline-offset: 2px;
}
.dashboard-summary-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.dashboard-summary-grid .metric {
min-height: 78px;
}
.dashboard-summary-grid .metric .value {
font-size: 25px;
}
.dashboard-trends-panel,
.dashboard-hosts-panel {
overflow: visible;
}
.panel {
margin-bottom: 18px;
overflow: auto;
@@ -304,17 +317,23 @@
}
.inline-form { margin: 0; }
.dashboard-priority-grid {
align-items: start;
display: grid;
gap: 14px;
grid-template-columns: minmax(280px, 1.25fr) repeat(3, minmax(220px, 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-bottom: 20px;
}
.priority-panel {
display: grid;
gap: 12px;
margin-bottom: 0;
min-width: 0;
overflow: visible;
}
.priority-panel > h2:first-child {
flex-wrap: wrap;
margin-bottom: 0;
}
.priority-panel > h2:first-child { margin-bottom: 0; }
.action-list,
.activity-list,
.schedule-list {
@@ -385,6 +404,9 @@
.activity-row {
grid-template-columns: max-content minmax(0, 1fr);
}
.activity-row .status {
justify-self: start;
}
.schedule-row {
grid-template-columns: minmax(0, 1fr) max-content;
}
@@ -404,6 +426,14 @@
gap: 2px;
min-width: 0;
}
.action-row strong,
.action-row .muted,
.activity-row strong,
.activity-row .muted,
.schedule-row strong,
.schedule-row .muted {
overflow-wrap: anywhere;
}
.schedule-time {
justify-items: end;
text-align: right;
@@ -436,6 +466,10 @@
justify-content: space-between;
padding-top: 8px;
}
.storage-priority-facts strong {
text-align: right;
overflow-wrap: anywhere;
}
.host-control-grid {
display: grid;
gap: 14px;
@@ -572,6 +606,7 @@
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
min-width: 0;
padding: 16px;
}
.host-card:hover {
@@ -605,8 +640,8 @@
}
.host-card-layout {
display: grid;
gap: 24px;
grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr);
gap: 18px;
grid-template-columns: minmax(0, 1.7fr) minmax(240px, 0.9fr);
}
.host-card-section {
align-content: start;
@@ -623,7 +658,7 @@
.host-card-timeline {
display: grid;
gap: 16px 22px;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.host-card-stats {
align-content: start;
@@ -649,6 +684,10 @@
.host-card-item .value {
overflow-wrap: anywhere;
}
.host-card-item .value a {
overflow-wrap: anywhere;
word-break: break-word;
}
.host-card-stat {
display: grid;
gap: 3px;
@@ -679,6 +718,9 @@
margin-top: 14px;
padding: 10px;
}
.host-card-warning > * {
min-width: 0;
}
.messages { display: grid; gap: 8px; margin-bottom: 18px; }
.message {
background: var(--panel);
@@ -778,6 +820,46 @@
.host-card-stats { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.insight-grid { grid-template-columns: 1fr; }
}
@media (max-width: 1100px) {
.dashboard-priority-grid {
grid-template-columns: 1fr;
}
.dashboard-summary-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.host-card-layout {
grid-template-columns: 1fr;
}
.host-card-status {
justify-content: flex-start;
max-width: none;
}
.schedule-row {
grid-template-columns: 1fr;
}
.schedule-time {
justify-items: start;
text-align: left;
}
}
@media (max-width: 560px) {
.dashboard-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.metric {
min-height: 76px;
padding: 12px;
}
.metric .value {
font-size: 24px;
}
.host-card {
padding: 13px;
}
.host-card-stats {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>

View File

@@ -33,7 +33,7 @@
{% endif %}
<section class="dashboard-priority-grid" aria-label="Operator priorities">
<article class="panel priority-panel">
<article class="panel priority-panel dashboard-panel-required">
<h2>Required Action</h2>
{% if action_items %}
<div class="action-list">
@@ -70,7 +70,7 @@
{% endif %}
</article>
<article class="panel priority-panel">
<article class="panel priority-panel dashboard-panel-schedules">
<h2>Next Scheduled Work <a class="button-link secondary compact" href="{% url 'schedules_list' %}">View all</a></h2>
{% if next_schedule_rows %}
<div class="schedule-list">
@@ -96,7 +96,7 @@
{% endif %}
</article>
<article class="panel priority-panel">
<article class="panel priority-panel dashboard-panel-activity">
<h2>Recent Activity <a class="button-link secondary compact" href="{% url 'runs_list' %}">View all</a></h2>
{% if recent_runs %}
<div class="activity-list">
@@ -115,7 +115,7 @@
{% endif %}
</article>
<article class="panel priority-panel">
<article class="panel priority-panel dashboard-panel-storage">
<h2>Storage Pressure</h2>
{% if stats_summary.runs_sampled %}
<div class="storage-priority">
@@ -163,7 +163,7 @@
</article>
</section>
<section class="grid" aria-label="Summary">
<section class="grid dashboard-summary-grid" aria-label="Summary">
<a class="metric metric-link" href="#hosts"><div class="label">Hosts</div><div class="value">{{ counts.enabled_hosts }}/{{ counts.hosts }}</div></a>
<a class="metric metric-link" href="{% url 'schedules_list' %}"><div class="label">Schedules</div><div class="value">{{ counts.enabled_schedules }}/{{ counts.schedules }}</div></a>
<a class="metric metric-link" href="{% url 'snapshots_list' %}"><div class="label">Snapshots</div><div class="value">{{ counts.snapshots }}</div></a>
@@ -172,7 +172,7 @@
<a class="metric metric-link {% if counts.failed_runs %}failed{% endif %}" href="{% url 'runs_list' %}?status=failed&amp;review=needed"><div class="label">Failed</div><div class="value">{{ counts.failed_runs }}</div></a>
</section>
<section class="panel">
<section class="panel dashboard-trends-panel">
<h2>Backup Trends</h2>
{% if stats_summary.runs_sampled %}
<div class="insight-grid" aria-label="Backup trends">
@@ -216,7 +216,7 @@
{% endif %}
</section>
<section class="panel" id="hosts">
<section class="panel dashboard-hosts-panel" id="hosts">
<h2>Hosts</h2>
<div class="host-list">
{% for host in hosts %}

View File

@@ -103,6 +103,13 @@ class ViewTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Control panel")
self.assertContains(response, "Backup health, required action, storage pressure, and recent activity in one place.")
self.assertContains(response, "dashboard-panel-required")
self.assertContains(response, "dashboard-panel-schedules")
self.assertContains(response, "dashboard-panel-activity")
self.assertContains(response, "dashboard-panel-storage")
self.assertContains(response, "dashboard-summary-grid")
self.assertContains(response, "dashboard-trends-panel")
self.assertContains(response, "dashboard-hosts-panel")
self.assertContains(response, "Dashboard")
self.assertContains(response, "web-01")
self.assertContains(response, "20260519-021500Z__ABCDEFGH")