Skip to content

Commit 23d6730

Browse files
itsDNNSclaude
andcommitted
Redesign dashboard summary: info bar + 3 metric cards with progressive disclosure
Replace 7 flat summary tiles with a compact connection info bar and 3 color-coded metric cards (Signal Quality, Upstream, Errors). Each card shows health-colored border, gauge bars, and expandable detail tables. Add uncorr_pct log-scale computation in web.py and 13 new i18n keys in EN/DE/FR/ES. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8cb722b commit 23d6730

File tree

6 files changed

+274
-38
lines changed

6 files changed

+274
-38
lines changed

app/i18n/de.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,19 @@
160160
"bqm_optional": "optional",
161161
"bqm_share_url": "BQM Share-URL",
162162
"bqm_url_hint": "ThinkBroadband BQM Share-Link einfuegen (.png)",
163-
"bqm_no_data": "Kein BQM-Graph fuer dieses Datum."
163+
"bqm_no_data": "Kein BQM-Graph fuer dieses Datum.",
164+
165+
"card_signal_quality": "Signalqualitaet",
166+
"card_upstream": "Upstream",
167+
"card_errors": "Fehler",
168+
"card_ds_power": "Signalpegel",
169+
"card_snr": "Signalklarheit",
170+
"card_transmit_power": "Sendeleistung",
171+
"card_uncorr_label": "unkorrigierbar",
172+
"card_corr_label": "automatisch korrigiert",
173+
"card_power_range": "Bereich",
174+
"card_ideal_range": "Idealbereich",
175+
"card_uncorr_exact": "Unkorrigierbar (exakt)",
176+
"card_corr_exact": "Korrigierbar (exakt)",
177+
"card_errors_note": "Unkorrigierbare Fehler koennen nicht wiederhergestellt werden. Hohe und steigende Werte deuten auf ein Kabelproblem hin."
164178
}

app/i18n/en.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,19 @@
160160
"bqm_optional": "optional",
161161
"bqm_share_url": "BQM Share URL",
162162
"bqm_url_hint": "Paste your ThinkBroadband BQM share link (.png)",
163-
"bqm_no_data": "No BQM graph for this date."
163+
"bqm_no_data": "No BQM graph for this date.",
164+
165+
"card_signal_quality": "Signal Quality",
166+
"card_upstream": "Upstream",
167+
"card_errors": "Errors",
168+
"card_ds_power": "Signal Level",
169+
"card_snr": "Signal Clarity",
170+
"card_transmit_power": "Transmit Power",
171+
"card_uncorr_label": "uncorrectable",
172+
"card_corr_label": "corrected automatically",
173+
"card_power_range": "Range",
174+
"card_ideal_range": "Ideal Range",
175+
"card_uncorr_exact": "Uncorrectable (exact)",
176+
"card_corr_exact": "Correctable (exact)",
177+
"card_errors_note": "Uncorrectable errors cannot be recovered. High growing numbers indicate a cable issue."
164178
}

app/i18n/es.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,19 @@
160160
"bqm_optional": "opcional",
161161
"bqm_share_url": "URL de compartir BQM",
162162
"bqm_url_hint": "Pega tu enlace de compartir ThinkBroadband BQM (.png)",
163-
"bqm_no_data": "No hay gráfico BQM para esta fecha."
163+
"bqm_no_data": "No hay gráfico BQM para esta fecha.",
164+
165+
"card_signal_quality": "Calidad de señal",
166+
"card_upstream": "Ascendente",
167+
"card_errors": "Errores",
168+
"card_ds_power": "Nivel de señal",
169+
"card_snr": "Claridad de señal",
170+
"card_transmit_power": "Potencia de transmisión",
171+
"card_uncorr_label": "no corregibles",
172+
"card_corr_label": "corregidos automáticamente",
173+
"card_power_range": "Rango",
174+
"card_ideal_range": "Rango ideal",
175+
"card_uncorr_exact": "No corregibles (exacto)",
176+
"card_corr_exact": "Corregibles (exacto)",
177+
"card_errors_note": "Los errores no corregibles no se pueden recuperar. Números altos y crecientes indican un problema de cable."
164178
}

app/i18n/fr.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,19 @@
160160
"bqm_optional": "optionnel",
161161
"bqm_share_url": "URL de partage BQM",
162162
"bqm_url_hint": "Collez votre lien de partage ThinkBroadband BQM (.png)",
163-
"bqm_no_data": "Pas de graphique BQM pour cette date."
163+
"bqm_no_data": "Pas de graphique BQM pour cette date.",
164+
165+
"card_signal_quality": "Qualité du signal",
166+
"card_upstream": "Montant",
167+
"card_errors": "Erreurs",
168+
"card_ds_power": "Niveau du signal",
169+
"card_snr": "Clarté du signal",
170+
"card_transmit_power": "Puissance d'émission",
171+
"card_uncorr_label": "non corrigeables",
172+
"card_corr_label": "corrigées automatiquement",
173+
"card_power_range": "Plage",
174+
"card_ideal_range": "Plage idéale",
175+
"card_uncorr_exact": "Non corrigeables (exact)",
176+
"card_corr_exact": "Corrigeables (exact)",
177+
"card_errors_note": "Les erreurs non corrigeables ne peuvent pas être récupérées. Des chiffres élevés et croissants indiquent un problème de câble."
164178
}

app/templates/index.html

Lines changed: 204 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,96 @@
197197
.health-issue-name { font-weight: bold; font-size: 0.9em; display: block; }
198198
.health-issue-desc { font-size: 0.82em; color: var(--muted); margin-top: 2px; display: block; }
199199
h2.section-title { font-size: 1.1em; margin: 16px 0 8px; color: var(--accent); }
200-
.summary-grid {
201-
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
202-
gap: 10px; margin-bottom: 16px;
200+
/* ── Connection Info Bar ── */
201+
.conn-info-bar {
202+
background: var(--surface); border-radius: 6px; padding: 10px 16px;
203+
font-size: 0.9em; color: var(--muted); margin-bottom: 12px;
204+
display: flex; align-items: center; gap: 6px;
205+
}
206+
.conn-info-bar .conn-sep { opacity: 0.4; }
207+
208+
/* ── Metric Cards Grid ── */
209+
.metric-cards {
210+
display: grid; grid-template-columns: repeat(3, 1fr);
211+
gap: 12px; margin-bottom: 16px;
212+
}
213+
214+
/* ── Metric Card ── */
215+
.metric-card {
216+
background: var(--surface); border-radius: 6px;
217+
border-left: 4px solid var(--muted); overflow: hidden;
218+
}
219+
.metric-card.health-good { border-left-color: var(--good); }
220+
.metric-card.health-warn { border-left-color: var(--warn); }
221+
.metric-card.health-crit { border-left-color: var(--crit); }
222+
223+
.metric-card-header {
224+
display: flex; align-items: center; gap: 8px;
225+
padding: 12px 14px 0; cursor: pointer; user-select: none;
226+
}
227+
.metric-card-header .status-dot {
228+
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
229+
}
230+
.health-good .status-dot { background: var(--good); }
231+
.health-warn .status-dot { background: var(--warn); }
232+
.health-crit .status-dot { background: var(--crit); }
233+
.metric-card-header .card-title {
234+
font-size: 0.75em; font-weight: 700; text-transform: uppercase;
235+
letter-spacing: 0.05em; color: var(--muted); flex: 1;
236+
}
237+
.metric-card-header .chevron {
238+
font-size: 0.7em; color: var(--muted); transition: transform 0.25s ease;
239+
}
240+
.metric-card.open .metric-card-header .chevron { transform: rotate(90deg); }
241+
242+
.metric-card-body { padding: 8px 14px 14px; }
243+
244+
/* ── Metric Row (side-by-side metrics) ── */
245+
.metric-row { display: flex; gap: 20px; }
246+
.metric-item { flex: 1; min-width: 0; }
247+
.metric-value {
248+
font-size: 1.6em; font-weight: bold; line-height: 1.2;
249+
}
250+
.metric-value.val-good { color: var(--good); }
251+
.metric-value.val-warn { color: var(--warn); }
252+
.metric-value.val-crit { color: var(--crit); }
253+
.metric-unit { font-size: 0.7em; font-weight: normal; color: var(--muted); margin-left: 2px; }
254+
.metric-label { font-size: 0.78em; color: var(--muted); margin-top: 2px; }
255+
256+
/* ── Gauge Bar ── */
257+
.gauge-bar {
258+
height: 4px; background: rgba(255,255,255,0.08); border-radius: 2px;
259+
margin-top: 6px; overflow: hidden;
260+
}
261+
[data-theme="light"] .gauge-bar { background: rgba(0,0,0,0.08); }
262+
.gauge-fill { height: 100%; border-radius: 2px; transition: width 0.4s ease; }
263+
.gauge-fill.val-good { background: var(--good); }
264+
.gauge-fill.val-warn { background: var(--warn); }
265+
.gauge-fill.val-crit { background: var(--crit); }
266+
267+
/* ── Secondary Value (errors card) ── */
268+
.metric-secondary {
269+
font-size: 0.85em; color: var(--muted); margin-top: 6px;
270+
}
271+
.metric-secondary strong { color: var(--text); font-weight: 600; }
272+
273+
/* ── Card Detail (expandable) ── */
274+
.metric-card-detail {
275+
max-height: 0; overflow: hidden;
276+
transition: max-height 0.3s ease;
277+
padding: 0 14px;
278+
}
279+
.metric-card.open .metric-card-detail { max-height: 300px; }
280+
.detail-table {
281+
width: 100%; font-size: 0.82em; border-collapse: collapse;
282+
margin: 8px 0 12px;
283+
}
284+
.detail-table td { padding: 4px 0; }
285+
.detail-table td:first-child { color: var(--muted); padding-right: 12px; }
286+
.detail-note {
287+
font-size: 0.78em; color: var(--muted); font-style: italic;
288+
padding-bottom: 12px;
203289
}
204-
.summary-card { background: var(--surface); border-radius: 6px; padding: 12px; }
205-
.summary-card .label { font-size: 0.8em; color: var(--muted); }
206-
.summary-card .value { font-size: 1.3em; font-weight: bold; margin-top: 4px; }
207290

208291
/* ── Tables ── */
209292
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 0.9em; }
@@ -325,11 +408,15 @@
325408
.sidebar { width: 0; min-width: 0; }
326409
.sidebar.collapsed { width: 0; min-width: 0; }
327410
}
411+
@media (max-width: 900px) {
412+
.metric-cards { grid-template-columns: 1fr; }
413+
}
328414
@media (max-width: 600px) {
329415
body { font-size: 14px; }
330416
.topbar { gap: 6px; padding: 8px 10px; }
331417
.topbar-title { display: none; }
332-
.summary-grid { grid-template-columns: 1fr 1fr; }
418+
.metric-cards { grid-template-columns: 1fr; }
419+
.metric-row { flex-direction: column; gap: 10px; }
333420
table { font-size: 0.8em; }
334421
th, td { padding: 4px 6px; }
335422
.calendar-popup { left: 10px; right: 10px; min-width: auto; }
@@ -480,36 +567,116 @@
480567
{% endif %}
481568
</div>
482569

483-
<div class="summary-grid">
484-
{% if isp_name or connection_info %}
485-
<div class="summary-card">
486-
<div class="label">{{ t.tariff }}</div>
487-
<div class="value">{% if isp_name %}{{ isp_name }}{% endif %}{% if connection_info.max_downstream_kbps %} {{ connection_info.max_downstream_kbps // 1000 }}/{{ connection_info.max_upstream_kbps // 1000 }} Mbit/s{% endif %}</div>
488-
</div>
570+
{# ── Connection Info Bar ── #}
571+
{% if isp_name or (connection_info and connection_info.max_downstream_kbps) %}
572+
<div class="conn-info-bar">
573+
{% if isp_name %}{{ isp_name }}{% endif %}
574+
{% if connection_info and connection_info.max_downstream_kbps %}
575+
{% if isp_name %}<span class="conn-sep">&middot;</span>{% endif %}
576+
{{ connection_info.max_downstream_kbps // 1000 }}/{{ connection_info.max_upstream_kbps // 1000 }} Mbit/s
489577
{% endif %}
490-
<div class="summary-card">
491-
<div class="label">{{ t.ds_channels }}</div>
492-
<div class="value">{{ s.ds_total }}</div>
493-
</div>
494-
<div class="summary-card">
495-
<div class="label">{{ t.ds_power_range }}</div>
496-
<div class="value">{{ s.ds_power_min }} / {{ s.ds_power_avg }} / {{ s.ds_power_max }} dBmV</div>
497-
</div>
498-
<div class="summary-card">
499-
<div class="label">{{ t.ds_snr_range }}</div>
500-
<div class="value">{{ s.ds_snr_min }} / {{ s.ds_snr_avg }} dB</div>
501-
</div>
502-
<div class="summary-card">
503-
<div class="label">{{ t.ds_errors_label }}</div>
504-
<div class="value">{{ s.ds_correctable_errors|fmt_k }} / {{ s.ds_uncorrectable_errors|fmt_k }}</div>
578+
<span class="conn-sep">|</span>
579+
{{ s.ds_total }} DS <span class="conn-sep">/</span> {{ s.us_total }} US {{ t.channels|lower }}
580+
</div>
581+
{% endif %}
582+
583+
{# ── Per-card health ── #}
584+
{% set signal_health = 'crit' if 'ds_power_critical' in s.health_issues or 'snr_critical' in s.health_issues
585+
else ('warn' if 'snr_warn' in s.health_issues else 'good') %}
586+
{% set us_health = 'crit' if 'us_power_critical' in s.health_issues
587+
else ('warn' if 'us_power_warn' in s.health_issues else 'good') %}
588+
{% set error_health = 'crit' if 'uncorr_errors_high' in s.health_issues else 'good' %}
589+
590+
{# ── Gauge percentages ── #}
591+
{% set ds_power_pct = [0, [100, (s.ds_power_avg|abs / 12 * 100)|int]|min]|max %}
592+
{% set snr_pct = [0, [100, ((s.ds_snr_avg - 20) / 25 * 100)|int]|min]|max %}
593+
{% set us_power_pct = [0, [100, ((s.us_power_avg - 30) / 30 * 100)|int]|min]|max %}
594+
595+
{# ── DS Power health ── #}
596+
{% set ds_pwr_health = 'crit' if 'ds_power_critical' in s.health_issues else 'good' %}
597+
{# ── SNR health ── #}
598+
{% set snr_health = 'crit' if 'snr_critical' in s.health_issues else ('warn' if 'snr_warn' in s.health_issues else 'good') %}
599+
600+
<div class="metric-cards">
601+
{# ── Card 1: Signal Quality ── #}
602+
<div class="metric-card health-{{ signal_health }}" onclick="toggleCard(this)">
603+
<div class="metric-card-header">
604+
<span class="status-dot"></span>
605+
<span class="card-title">{{ t.card_signal_quality }}</span>
606+
<span class="chevron">&#9654;</span>
607+
</div>
608+
<div class="metric-card-body">
609+
<div class="metric-row">
610+
<div class="metric-item">
611+
<div class="metric-value val-{{ ds_pwr_health }}">{{ s.ds_power_avg }}<span class="metric-unit">dBmV</span></div>
612+
<div class="metric-label">{{ t.card_ds_power }}</div>
613+
<div class="gauge-bar"><div class="gauge-fill val-{{ ds_pwr_health }}" style="width:{{ ds_power_pct }}%"></div></div>
614+
</div>
615+
<div class="metric-item">
616+
<div class="metric-value val-{{ snr_health }}">{{ s.ds_snr_avg }}<span class="metric-unit">dB</span></div>
617+
<div class="metric-label">{{ t.card_snr }}</div>
618+
<div class="gauge-bar"><div class="gauge-fill val-{{ snr_health }}" style="width:{{ snr_pct }}%"></div></div>
619+
</div>
620+
</div>
621+
</div>
622+
<div class="metric-card-detail">
623+
<table class="detail-table">
624+
<tr><td>{{ t.card_power_range }}</td><td>{{ s.ds_power_min }} ... {{ s.ds_power_max }} dBmV</td></tr>
625+
<tr><td>SNR {{ t.card_power_range }}</td><td>{{ s.ds_snr_min }} ... {{ s.ds_snr_avg }} dB</td></tr>
626+
<tr><td>{{ t.channels }}</td><td>{{ s.ds_total }} {{ t.downstream|lower }}</td></tr>
627+
<tr><td>{{ t.card_ideal_range }}</td><td>-7...+7 dBmV / SNR &gt;30 dB</td></tr>
628+
</table>
629+
</div>
505630
</div>
506-
<div class="summary-card">
507-
<div class="label">{{ t.us_channels }}</div>
508-
<div class="value">{{ s.us_total }}</div>
631+
632+
{# ── Card 2: Upstream ── #}
633+
<div class="metric-card health-{{ us_health }}" onclick="toggleCard(this)">
634+
<div class="metric-card-header">
635+
<span class="status-dot"></span>
636+
<span class="card-title">{{ t.card_upstream }}</span>
637+
<span class="chevron">&#9654;</span>
638+
</div>
639+
<div class="metric-card-body">
640+
<div class="metric-row">
641+
<div class="metric-item">
642+
<div class="metric-value val-{{ us_health }}">{{ s.us_power_avg }}<span class="metric-unit">dBmV</span></div>
643+
<div class="metric-label">{{ t.card_transmit_power }}</div>
644+
<div class="gauge-bar"><div class="gauge-fill val-{{ us_health }}" style="width:{{ us_power_pct }}%"></div></div>
645+
</div>
646+
</div>
647+
</div>
648+
<div class="metric-card-detail">
649+
<table class="detail-table">
650+
<tr><td>{{ t.card_power_range }}</td><td>{{ s.us_power_min }} ... {{ s.us_power_max }} dBmV</td></tr>
651+
<tr><td>{{ t.channels }}</td><td>{{ s.us_total }} {{ t.upstream|lower }}</td></tr>
652+
<tr><td>{{ t.card_ideal_range }}</td><td>35 ... 49 dBmV</td></tr>
653+
</table>
654+
</div>
509655
</div>
510-
<div class="summary-card">
511-
<div class="label">{{ t.us_power_range }}</div>
512-
<div class="value">{{ s.us_power_min }} / {{ s.us_power_avg }} / {{ s.us_power_max }} dBmV</div>
656+
657+
{# ── Card 3: Errors ── #}
658+
<div class="metric-card health-{{ error_health }}" onclick="toggleCard(this)">
659+
<div class="metric-card-header">
660+
<span class="status-dot"></span>
661+
<span class="card-title">{{ t.card_errors }}</span>
662+
<span class="chevron">&#9654;</span>
663+
</div>
664+
<div class="metric-card-body">
665+
<div class="metric-row">
666+
<div class="metric-item">
667+
<div class="metric-value val-{{ error_health }}">{{ s.ds_uncorrectable_errors|fmt_k }}<span class="metric-unit">{{ t.card_uncorr_label }}</span></div>
668+
<div class="gauge-bar"><div class="gauge-fill val-{{ error_health }}" style="width:{{ uncorr_pct|int }}%"></div></div>
669+
<div class="metric-secondary">{{ s.ds_correctable_errors|fmt_k }} {{ t.card_corr_label }}</div>
670+
</div>
671+
</div>
672+
</div>
673+
<div class="metric-card-detail">
674+
<table class="detail-table">
675+
<tr><td>{{ t.card_uncorr_exact }}</td><td>{{ "{:,}".format(s.ds_uncorrectable_errors) }}</td></tr>
676+
<tr><td>{{ t.card_corr_exact }}</td><td>{{ "{:,}".format(s.ds_correctable_errors) }}</td></tr>
677+
</table>
678+
<div class="detail-note">{{ t.card_errors_note }}</div>
679+
</div>
513680
</div>
514681
</div>
515682

@@ -1058,6 +1225,9 @@ <h2>{{ t.export_title }}</h2>
10581225
setTimeout(function() { btn.textContent = T.copy_clipboard; }, 3000);
10591226
}
10601227
}
1228+
function toggleCard(el) {
1229+
el.classList.toggle('open');
1230+
}
10611231
</script>
10621232

10631233
</body>

app/web.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import functools
44
import logging
5+
import math
56
import os
67
import re
78
import stat
@@ -185,6 +186,13 @@ def index():
185186
bqm_configured = _config_manager.is_bqm_configured() if _config_manager else False
186187
conn_info = _state.get("connection_info") or {}
187188

189+
def _compute_uncorr_pct(analysis):
190+
"""Compute log-scale percentage for uncorrectable errors gauge."""
191+
if not analysis:
192+
return 0
193+
uncorr = analysis.get("summary", {}).get("ds_uncorrectable_errors", 0)
194+
return min(100, math.log10(max(1, uncorr)) / 5 * 100)
195+
188196
ts = request.args.get("t")
189197
if ts and not _TS_RE.match(ts):
190198
return redirect("/")
@@ -202,6 +210,7 @@ def index():
202210
theme=theme,
203211
isp_name=isp_name, connection_info=conn_info,
204212
bqm_configured=bqm_configured,
213+
uncorr_pct=_compute_uncorr_pct(snapshot),
205214
t=t, lang=lang, languages=LANGUAGES,
206215
)
207216
return render_template(
@@ -215,6 +224,7 @@ def index():
215224
theme=theme,
216225
isp_name=isp_name, connection_info=conn_info,
217226
bqm_configured=bqm_configured,
227+
uncorr_pct=_compute_uncorr_pct(_state["analysis"]),
218228
t=t, lang=lang, languages=LANGUAGES,
219229
)
220230

0 commit comments

Comments
 (0)