Skip to content

Commit c48b4f3

Browse files
itsDNNSclaude
andcommitted
Redesign health status with translated issues and actionable guidance
Health banner now shows status icon, translated explanation, and per-issue descriptions with recommendations. Analyzer uses English keys (good/marginal/poor) translated in UI. Channel groups collapsed by default. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ee51f15 commit c48b4f3

File tree

5 files changed

+139
-47
lines changed

5 files changed

+139
-47
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
Versioning: `YYYY-MM-DD.N` (date + sequential build number per day)
66

7+
## [2026-02-09.10]
8+
9+
### Changed
10+
- **Health status redesigned**: Informative banner with translated status, explanation, and per-issue descriptions with actionable recommendations
11+
- **Analyzer outputs English keys**: Health uses `good/marginal/poor`, issues are machine-readable keys translated in the UI
12+
- **Channel groups collapsed by default**: Less visual clutter, expand to inspect
13+
714
## [2026-02-09.9]
815

916
### Added

app/analyzer.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,10 @@ def _channel_health(issues):
3838

3939

4040
def _health_detail(issues):
41-
"""Build a human-readable detail string from issue list."""
41+
"""Build a machine-readable detail string from issue list."""
4242
if not issues:
4343
return ""
44-
labels = []
45-
for i in issues:
46-
if "power" in i and "critical" in i:
47-
labels.append("Power kritisch")
48-
elif "power" in i:
49-
labels.append("Power erhoeht")
50-
if "snr" in i and "critical" in i:
51-
labels.append("SNR kritisch")
52-
elif "snr" in i:
53-
labels.append("SNR niedrig")
54-
return " + ".join(labels) if labels else ""
44+
return " + ".join(issues)
5545

5646

5747
def _assess_ds_channel(ch, docsis_ver):
@@ -201,25 +191,25 @@ def analyze(data: dict) -> dict:
201191
# --- Overall health ---
202192
issues = []
203193
if ds_powers and (min(ds_powers) < -DS_POWER_CRIT or max(ds_powers) > DS_POWER_CRIT):
204-
issues.append("DS Power ausserhalb Norm")
194+
issues.append("ds_power_critical")
205195
if us_powers and max(us_powers) > US_POWER_CRIT:
206-
issues.append("US Power kritisch hoch")
196+
issues.append("us_power_critical")
207197
elif us_powers and max(us_powers) > US_POWER_WARN:
208-
issues.append("US Power erhoeht")
198+
issues.append("us_power_warn")
209199
if ds_snrs and min(ds_snrs) < SNR_CRIT:
210-
issues.append("SNR zu niedrig")
200+
issues.append("snr_critical")
211201
elif ds_snrs and min(ds_snrs) < SNR_WARN:
212-
issues.append("SNR grenzwertig")
202+
issues.append("snr_warn")
213203
if total_uncorr > UNCORR_ERRORS_CRIT:
214-
issues.append("Viele uncorrectable Errors")
204+
issues.append("uncorr_errors_high")
215205

216206
if not issues:
217-
summary["health"] = "Gut"
218-
elif any("kritisch" in i for i in issues):
219-
summary["health"] = "Schlecht"
207+
summary["health"] = "good"
208+
elif any("critical" in i for i in issues):
209+
summary["health"] = "poor"
220210
else:
221-
summary["health"] = "Grenzwertig"
222-
summary["health_details"] = "; ".join(issues) if issues else "Alles OK"
211+
summary["health"] = "marginal"
212+
summary["health_issues"] = issues
223213

224214
log.info(
225215
"Analysis: DS=%d US=%d Health=%s",

app/i18n.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,37 @@
1616
"open_calendar": "Open calendar",
1717
"last_update": "Last Update",
1818

19-
# Health (display labels for analyzer values Gut/Grenzwertig/Schlecht)
19+
# Health status
2020
"health_good": "Good",
2121
"health_marginal": "Marginal",
2222
"health_poor": "Poor",
2323
"health_warning": "Warning",
2424
"health_critical": "Critical",
25+
"health_good_msg": "Your connection looks healthy. All values are within normal range.",
26+
"health_marginal_msg": "Some values are outside the ideal range. This may not affect your internet experience yet, but is worth monitoring.",
27+
"health_poor_msg": "One or more values are critically out of range. You may experience connection issues such as slow speeds, dropouts, or high latency.",
28+
"all_ok": "All channels operating normally.",
29+
"issues_found": "Issues detected",
30+
31+
# Health issue descriptions
32+
"issue_ds_power_critical": "Downstream power out of spec",
33+
"issue_ds_power_critical_desc": "Signal level exceeds +/-10 dBmV. This usually indicates a cabling or amplifier problem. Contact your ISP if the issue persists.",
34+
"issue_us_power_critical": "Upstream power critically high",
35+
"issue_us_power_critical_desc": "Transmit power exceeds 54 dBmV. Your modem is working too hard to reach the provider. Check cable connections or request a signal check from your ISP.",
36+
"issue_us_power_warn": "Upstream power elevated",
37+
"issue_us_power_warn_desc": "Transmit power is between 50-54 dBmV (ideal: 35-49). May indicate signal loss on the line. Monitor this value over time.",
38+
"issue_snr_critical": "Signal-to-noise ratio critically low",
39+
"issue_snr_critical_desc": "SNR below 25 dB causes packet loss and connection instability. Common causes: damaged coax cable, loose connectors, or interference from splitters.",
40+
"issue_snr_warn": "Signal-to-noise ratio below ideal",
41+
"issue_snr_warn_desc": "SNR between 25-30 dB (ideal: above 30 dB). Connection works but has reduced margin for error. Consider checking cable quality.",
42+
"issue_uncorr_errors_high": "High uncorrectable error count",
43+
"issue_uncorr_errors_high_desc": "Over 10,000 uncorrectable errors detected. These cannot be recovered and cause data loss. If this number grows rapidly, contact your ISP.",
44+
45+
# Channel health tooltips
46+
"ch_power_critical": "Power critically out of range",
47+
"ch_power_warning": "Power slightly elevated",
48+
"ch_snr_critical": "SNR critically low",
49+
"ch_snr_warning": "SNR below ideal",
2550

2651
# Summary cards
2752
"ds_channels": "Downstream Channels",
@@ -157,6 +182,29 @@
157182
"health_poor": "Schlecht",
158183
"health_warning": "Warnung",
159184
"health_critical": "Kritisch",
185+
"health_good_msg": "Deine Verbindung sieht gut aus. Alle Werte sind im Normalbereich.",
186+
"health_marginal_msg": "Einige Werte liegen ausserhalb des Idealbereichs. Das beeintraechtigt dein Internet moeglicherweise noch nicht, sollte aber beobachtet werden.",
187+
"health_poor_msg": "Ein oder mehrere Werte sind kritisch. Du koenntest Verbindungsprobleme wie langsame Geschwindigkeiten, Abbrueche oder hohe Latenz bemerken.",
188+
"all_ok": "Alle Kanaele arbeiten normal.",
189+
"issues_found": "Probleme erkannt",
190+
191+
"issue_ds_power_critical": "Downstream-Pegel ausserhalb der Spezifikation",
192+
"issue_ds_power_critical_desc": "Signalpegel ueberschreitet +/-10 dBmV. Das deutet meist auf ein Kabel- oder Verstaerkerproblem hin. Kontaktiere deinen Anbieter, wenn das Problem bestehen bleibt.",
193+
"issue_us_power_critical": "Upstream-Sendeleistung kritisch hoch",
194+
"issue_us_power_critical_desc": "Sendeleistung ueberschreitet 54 dBmV. Dein Modem arbeitet zu hart, um den Anbieter zu erreichen. Pruefe Kabelverbindungen oder fordere eine Signalmessung an.",
195+
"issue_us_power_warn": "Upstream-Sendeleistung erhoeht",
196+
"issue_us_power_warn_desc": "Sendeleistung zwischen 50-54 dBmV (ideal: 35-49). Kann auf Signalverlust hindeuten. Beobachte diesen Wert ueber Zeit.",
197+
"issue_snr_critical": "Signal-Rausch-Verhaeltnis kritisch niedrig",
198+
"issue_snr_critical_desc": "SNR unter 25 dB verursacht Paketverluste und Verbindungsinstabilitaet. Haeufige Ursachen: beschaedigtes Koax-Kabel, lose Stecker oder Stoerungen durch Splitter.",
199+
"issue_snr_warn": "Signal-Rausch-Verhaeltnis unter Idealwert",
200+
"issue_snr_warn_desc": "SNR zwischen 25-30 dB (ideal: ueber 30 dB). Verbindung funktioniert, hat aber weniger Fehlertoleranz. Pruefe die Kabelqualitaet.",
201+
"issue_uncorr_errors_high": "Hohe Anzahl unkorrigierbarer Fehler",
202+
"issue_uncorr_errors_high_desc": "Ueber 10.000 unkorrigierbare Fehler erkannt. Diese koennen nicht repariert werden und verursachen Datenverlust. Wenn diese Zahl schnell waechst, kontaktiere deinen Anbieter.",
203+
204+
"ch_power_critical": "Pegel kritisch ausserhalb des Bereichs",
205+
"ch_power_warning": "Pegel leicht erhoeht",
206+
"ch_snr_critical": "SNR kritisch niedrig",
207+
"ch_snr_warning": "SNR unter Idealwert",
160208

161209
"ds_channels": "Downstream Kanaele",
162210
"us_channels": "Upstream Kanaele",

app/templates/index.html

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,28 @@
173173
}
174174
.history-banner a { color: var(--info); font-size: 0.85em; }
175175
.health-banner {
176-
padding: 12px 20px; border-radius: 8px;
177-
font-size: 1.1em; font-weight: bold; margin-bottom: 16px;
178-
}
179-
.health-good { background: rgba(76,175,80,0.2); border: 1px solid var(--good); color: var(--good); }
180-
.health-warn { background: rgba(255,152,0,0.2); border: 1px solid var(--warn); color: var(--warn); }
181-
.health-crit { background: rgba(244,67,54,0.2); border: 1px solid var(--crit); color: var(--crit); }
176+
padding: 16px 20px; border-radius: 8px; margin-bottom: 16px;
177+
}
178+
.health-good { background: rgba(76,175,80,0.1); border: 1px solid var(--good); }
179+
.health-warn { background: rgba(255,152,0,0.1); border: 1px solid var(--warn); }
180+
.health-crit { background: rgba(244,67,54,0.1); border: 1px solid var(--crit); }
181+
.health-status { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
182+
.health-icon { font-size: 1.4em; }
183+
.health-good .health-icon { color: var(--good); }
184+
.health-warn .health-icon { color: var(--warn); }
185+
.health-crit .health-icon { color: var(--crit); }
186+
.health-title { font-size: 1.15em; font-weight: bold; }
187+
.health-good .health-title { color: var(--good); }
188+
.health-warn .health-title { color: var(--warn); }
189+
.health-crit .health-title { color: var(--crit); }
190+
.health-msg { font-size: 0.9em; color: var(--text); opacity: 0.85; margin-bottom: 4px; }
191+
.health-issues { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
192+
.health-issue {
193+
padding: 8px 12px; border-radius: 6px;
194+
background: rgba(0,0,0,0.15);
195+
}
196+
.health-issue-name { font-weight: bold; font-size: 0.9em; display: block; }
197+
.health-issue-desc { font-size: 0.82em; color: var(--muted); margin-top: 2px; display: block; }
182198
h2.section-title { font-size: 1.1em; margin: 16px 0 8px; color: var(--accent); }
183199
.summary-grid {
184200
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@@ -414,16 +430,43 @@
414430
{% set ds = analysis.ds_channels %}
415431
{% set us = analysis.us_channels %}
416432

417-
{% set health_labels = {"Gut": t.health_good, "Grenzwertig": t.health_marginal, "Schlecht": t.health_poor} %}
418-
{% set health_label = health_labels.get(s.health, s.health) %}
419-
420-
{% if s.health == "Gut" %}
421-
<div class="health-banner health-good">{{ health_label }} &mdash; {{ s.health_details }}</div>
422-
{% elif s.health == "Schlecht" %}
423-
<div class="health-banner health-crit">{{ health_label }} &mdash; {{ s.health_details }}</div>
424-
{% else %}
425-
<div class="health-banner health-warn">{{ health_label }} &mdash; {{ s.health_details }}</div>
426-
{% endif %}
433+
{% set health_map = {"good": t.health_good, "marginal": t.health_marginal, "poor": t.health_poor} %}
434+
{% set health_label = health_map.get(s.health, s.health) %}
435+
{% set health_msgs = {"good": t.health_good_msg, "marginal": t.health_marginal_msg, "poor": t.health_poor_msg} %}
436+
{% set issue_labels = {
437+
"ds_power_critical": t.issue_ds_power_critical,
438+
"us_power_critical": t.issue_us_power_critical,
439+
"us_power_warn": t.issue_us_power_warn,
440+
"snr_critical": t.issue_snr_critical,
441+
"snr_warn": t.issue_snr_warn,
442+
"uncorr_errors_high": t.issue_uncorr_errors_high,
443+
} %}
444+
{% set issue_descs = {
445+
"ds_power_critical": t.issue_ds_power_critical_desc,
446+
"us_power_critical": t.issue_us_power_critical_desc,
447+
"us_power_warn": t.issue_us_power_warn_desc,
448+
"snr_critical": t.issue_snr_critical_desc,
449+
"snr_warn": t.issue_snr_warn_desc,
450+
"uncorr_errors_high": t.issue_uncorr_errors_high_desc,
451+
} %}
452+
453+
<div class="health-banner health-{{ 'good' if s.health == 'good' else ('crit' if s.health == 'poor' else 'warn') }}">
454+
<div class="health-status">
455+
<span class="health-icon">{% if s.health == 'good' %}&#10004;{% elif s.health == 'poor' %}&#10006;{% else %}&#9888;{% endif %}</span>
456+
<span class="health-title">{{ health_label }}</span>
457+
</div>
458+
<div class="health-msg">{{ health_msgs.get(s.health, '') }}</div>
459+
{% if s.health_issues %}
460+
<div class="health-issues">
461+
{% for issue in s.health_issues %}
462+
<div class="health-issue">
463+
<span class="health-issue-name">{{ issue_labels.get(issue, issue) }}</span>
464+
<span class="health-issue-desc">{{ issue_descs.get(issue, '') }}</span>
465+
</div>
466+
{% endfor %}
467+
</div>
468+
{% endif %}
469+
</div>
427470

428471
<div class="summary-grid">
429472
{% if isp_name or connection_info %}
@@ -460,7 +503,7 @@
460503

461504
<h2 class="section-title">{{ t.downstream }} ({{ ds|length }} {{ t.channels }})</h2>
462505
{% for group in ds|sort(attribute='docsis_version', reverse=true)|groupby('docsis_version') %}
463-
<details class="channel-group" open>
506+
<details class="channel-group">
464507
<summary>DOCSIS {{ group.grouper }} ({{ group.list|length }} {{ t.channels }})</summary>
465508
<table class="sortable">
466509
<thead><tr>
@@ -470,10 +513,13 @@ <h2 class="section-title">{{ t.downstream }} ({{ ds|length }} {{ t.channels }})<
470513
<tbody>
471514
{% for ch in group.list %}
472515
<tr>
516+
{% set ch_tips = {"power critical": t.ch_power_critical, "power warning": t.ch_power_warning, "snr critical": t.ch_snr_critical, "snr warning": t.ch_snr_warning} %}
517+
{% set ch_tip_parts = [] %}
518+
{% for key, label in ch_tips.items() %}{% if key in ch.health_detail %}{% if ch_tip_parts.append(label) %}{% endif %}{% endif %}{% endfor %}
473519
<td data-sort="{{ 0 if ch.health == 'good' else (2 if ch.health == 'critical' else 1) }}">
474520
{% if ch.health == "good" %}<span class="badge badge-good">{{ t.health_good }}</span>
475-
{% elif ch.health == "critical" %}<span class="badge badge-crit" title="{{ ch.health_detail }}">{{ t.health_critical }}</span>
476-
{% else %}<span class="badge badge-warn" title="{{ ch.health_detail }}">{{ t.health_warning }}</span>{% endif %}
521+
{% elif ch.health == "critical" %}<span class="badge badge-crit" title="{{ ch_tip_parts|join(', ') }}">{{ t.health_critical }}</span>
522+
{% else %}<span class="badge badge-warn" title="{{ ch_tip_parts|join(', ') }}">{{ t.health_warning }}</span>{% endif %}
477523
</td>
478524
<td data-sort="{{ ch.channel_id }}">{{ ch.channel_id }}</td>
479525
<td>{{ ch.frequency }}</td>
@@ -491,7 +537,7 @@ <h2 class="section-title">{{ t.downstream }} ({{ ds|length }} {{ t.channels }})<
491537

492538
<h2 class="section-title">{{ t.upstream }} ({{ us|length }} {{ t.channels }})</h2>
493539
{% for group in us|sort(attribute='docsis_version', reverse=true)|groupby('docsis_version') %}
494-
<details class="channel-group" open>
540+
<details class="channel-group">
495541
<summary>DOCSIS {{ group.grouper }} ({{ group.list|length }} {{ t.channels }})</summary>
496542
<table class="sortable">
497543
<thead><tr>
@@ -500,11 +546,12 @@ <h2 class="section-title">{{ t.upstream }} ({{ us|length }} {{ t.channels }})</h
500546
</tr></thead>
501547
<tbody>
502548
{% for ch in group.list %}
549+
{% set ch_tip = t.ch_power_critical if 'power critical' in ch.health_detail else (t.ch_power_warning if 'power warning' in ch.health_detail else '') %}
503550
<tr>
504551
<td data-sort="{{ 0 if ch.health == 'good' else (2 if ch.health == 'critical' else 1) }}">
505552
{% if ch.health == "good" %}<span class="badge badge-good">{{ t.health_good }}</span>
506-
{% elif ch.health == "critical" %}<span class="badge badge-crit" title="{{ ch.health_detail }}">{{ t.health_critical }}</span>
507-
{% else %}<span class="badge badge-warn" title="{{ ch.health_detail }}">{{ t.health_warning }}</span>{% endif %}
553+
{% elif ch.health == "critical" %}<span class="badge badge-crit" title="{{ ch_tip }}">{{ t.health_critical }}</span>
554+
{% else %}<span class="badge badge-warn" title="{{ ch_tip }}">{{ t.health_warning }}</span>{% endif %}
508555
</td>
509556
<td data-sort="{{ ch.channel_id }}">{{ ch.channel_id }}</td>
510557
<td>{{ ch.frequency }}</td>

app/web.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def api_export():
291291
f"- **ISP**: {isp}" if isp else None,
292292
f"- **Tariff**: {ds_mbps}/{us_mbps} Mbit/s (Down/Up)" if ds_mbps else None,
293293
f"- **Health**: {s.get('health', 'Unknown')}",
294-
f"- **Details**: {s.get('health_details', '')}",
294+
f"- **Issues**: {', '.join(s.get('health_issues', []))}" if s.get('health_issues') else None,
295295
f"- **Timestamp**: {ts}",
296296
"",
297297
"## Summary",

0 commit comments

Comments
 (0)