Skip to content

Commit 854de10

Browse files
itsDNNSclaude
andcommitted
Group channels by DOCSIS version, move reference table to sidebar
- DS/US channels grouped into collapsible DOCSIS 3.1/3.0 sections - Reference values table moved to expandable sidebar section - Removed redundant DOCSIS column from channel tables Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 596abb2 commit 854de10

File tree

2 files changed

+165
-77
lines changed

2 files changed

+165
-77
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.4]
8+
9+
### Changed
10+
- **Channel tables grouped by DOCSIS version**: Collapsible sections for DOCSIS 3.1 and 3.0 channels
11+
- **Reference table moved to sidebar**: Accessible via expandable "Richtwerte" section in hamburger menu
12+
- **DOCSIS column removed**: Redundant with group headers, saves horizontal space
13+
714
## [2026-02-09.3]
815

916
### Added

app/templates/index.html

Lines changed: 158 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
border-right: 1px solid var(--input-border);
137137
}
138138
.sidebar.open { transform: translateX(0); }
139+
.sidebar-content { flex: 1; overflow-y: auto; }
139140
.sidebar-header {
140141
display: flex;
141142
justify-content: space-between;
@@ -306,8 +307,76 @@
306307
.val-good { color: var(--good); }
307308
.val-warn { color: var(--warn); }
308309
.val-crit { color: var(--crit); }
309-
.ref-table { max-width: 600px; }
310-
.ref-table td, .ref-table th { font-size: 0.85em; }
310+
/* ── Collapsible Channel Groups ── */
311+
details.channel-group {
312+
margin-bottom: 4px;
313+
}
314+
details.channel-group summary {
315+
cursor: pointer;
316+
padding: 8px 12px;
317+
background: var(--surface);
318+
border-radius: 6px;
319+
font-size: 0.95em;
320+
font-weight: bold;
321+
color: var(--accent);
322+
list-style: none;
323+
display: flex;
324+
align-items: center;
325+
gap: 8px;
326+
user-select: none;
327+
}
328+
details.channel-group summary::-webkit-details-marker { display: none; }
329+
details.channel-group summary::before {
330+
content: "\25B6";
331+
font-size: 0.65em;
332+
transition: transform 0.2s;
333+
}
334+
details.channel-group[open] summary::before {
335+
transform: rotate(90deg);
336+
}
337+
details.channel-group table { margin-top: 4px; }
338+
339+
/* ── Sidebar Reference Table ── */
340+
.sidebar-ref {
341+
padding: 0 20px 16px;
342+
}
343+
.sidebar-ref summary {
344+
cursor: pointer;
345+
font-size: 0.9em;
346+
color: var(--muted);
347+
padding: 10px 0;
348+
list-style: none;
349+
display: flex;
350+
align-items: center;
351+
gap: 8px;
352+
}
353+
.sidebar-ref summary::-webkit-details-marker { display: none; }
354+
.sidebar-ref summary::before {
355+
content: "\25B6";
356+
font-size: 0.6em;
357+
transition: transform 0.2s;
358+
}
359+
.sidebar-ref[open] summary::before {
360+
transform: rotate(90deg);
361+
}
362+
.sidebar-ref table {
363+
width: 100%;
364+
font-size: 0.78em;
365+
border-collapse: collapse;
366+
margin-top: 4px;
367+
}
368+
.sidebar-ref th, .sidebar-ref td {
369+
padding: 4px 6px;
370+
text-align: left;
371+
}
372+
.sidebar-ref th {
373+
color: var(--accent);
374+
position: static;
375+
background: transparent;
376+
}
377+
.sidebar-ref tr {
378+
border-bottom: 1px solid var(--input-border);
379+
}
311380
.error-box {
312381
background: rgba(244,67,54,0.15);
313382
border: 1px solid var(--crit);
@@ -393,22 +462,37 @@
393462
<span>Navigation</span>
394463
<button class="sidebar-close" id="sidebar-close">&times;</button>
395464
</div>
396-
<a class="sidebar-link active" data-view="live">
397-
<span class="icon">&#9679;</span> Live Dashboard
398-
</a>
399-
<a class="sidebar-link" data-view="day">
400-
<span class="icon">&#9201;</span> Tagesverlauf
401-
</a>
402-
<a class="sidebar-link" data-view="week">
403-
<span class="icon">&#9899;</span> Wochentrend
404-
</a>
405-
<a class="sidebar-link" data-view="month">
406-
<span class="icon">&#128197;</span> Monatstrend
407-
</a>
408-
<div class="sidebar-divider"></div>
409-
<a class="sidebar-link" href="/settings">
410-
<span class="icon">&#9881;</span> Einstellungen
411-
</a>
465+
<div class="sidebar-content">
466+
<a class="sidebar-link active" data-view="live">
467+
<span class="icon">&#9679;</span> Live Dashboard
468+
</a>
469+
<a class="sidebar-link" data-view="day">
470+
<span class="icon">&#9201;</span> Tagesverlauf
471+
</a>
472+
<a class="sidebar-link" data-view="week">
473+
<span class="icon">&#9899;</span> Wochentrend
474+
</a>
475+
<a class="sidebar-link" data-view="month">
476+
<span class="icon">&#128197;</span> Monatstrend
477+
</a>
478+
<div class="sidebar-divider"></div>
479+
<a class="sidebar-link" href="/settings">
480+
<span class="icon">&#9881;</span> Einstellungen
481+
</a>
482+
<div class="sidebar-divider"></div>
483+
<details class="sidebar-ref">
484+
<summary>Richtwerte</summary>
485+
<table>
486+
<thead><tr><th>Metrik</th><th>Gut</th><th>Grenz.</th><th>Schlecht</th></tr></thead>
487+
<tbody>
488+
<tr><td>DS Power</td><td>-7..+7</td><td>&plusmn;7..10</td><td>&gt;&plusmn;10 dBmV</td></tr>
489+
<tr><td>US Power</td><td>35..49</td><td>50..54</td><td>&gt;54 dBmV</td></tr>
490+
<tr><td>SNR/MER</td><td>&gt;30 dB</td><td>25..30</td><td>&lt;25 dB</td></tr>
491+
<tr><td>Unkorr.</td><td>niedrig</td><td>-</td><td>&gt;10.000</td></tr>
492+
</tbody>
493+
</table>
494+
</details>
495+
</div>
412496
</nav>
413497

414498
<!-- Topbar -->
@@ -500,67 +584,64 @@
500584
</div>
501585

502586
<h2 class="section-title">Downstream ({{ ds|length }} Kanaele)</h2>
503-
<table>
504-
<thead><tr>
505-
<th>Status</th><th>Kanal</th><th>Frequenz</th><th>Power (dBmV)</th>
506-
<th>SNR (dB)</th><th>Modulation</th><th>Korr.</th><th>Unkorr.</th><th>DOCSIS</th>
507-
</tr></thead>
508-
<tbody>
509-
{% for ch in ds %}
510-
<tr>
511-
<td>
512-
{% if ch.health == "good" %}<span class="badge badge-good">Gut</span>
513-
{% elif ch.health == "critical" %}<span class="badge badge-crit" title="{{ ch.health_detail }}">Kritisch</span>
514-
{% else %}<span class="badge badge-warn" title="{{ ch.health_detail }}">Warnung</span>{% endif %}
515-
</td>
516-
<td>{{ ch.channel_id }}</td>
517-
<td>{{ ch.frequency }}</td>
518-
<td class="{% if ch.power is not none %}{% if ch.power|abs > 10 %}val-crit{% elif ch.power|abs > 7 %}val-warn{% else %}val-good{% endif %}{% endif %}">{{ ch.power }}</td>
519-
<td class="{% if ch.snr is not none %}{% if ch.snr < 25 %}val-crit{% elif ch.snr < 30 %}val-warn{% else %}val-good{% endif %}{% endif %}">{{ ch.snr if ch.snr is not none else "-" }}</td>
520-
<td>{{ ch.modulation }}</td>
521-
<td>{{ "{:,}".format(ch.correctable_errors) }}</td>
522-
<td>{{ "{:,}".format(ch.uncorrectable_errors) }}</td>
523-
<td>{{ ch.docsis_version }}</td>
524-
</tr>
525-
{% endfor %}
526-
</tbody>
527-
</table>
587+
{% for group in ds|sort(attribute='docsis_version', reverse=true)|groupby('docsis_version') %}
588+
<details class="channel-group" open>
589+
<summary>DOCSIS {{ group.grouper }} ({{ group.list|length }} Kanaele)</summary>
590+
<table>
591+
<thead><tr>
592+
<th>Status</th><th>Kanal</th><th>Frequenz</th><th>Power (dBmV)</th>
593+
<th>SNR (dB)</th><th>Modulation</th><th>Korr.</th><th>Unkorr.</th>
594+
</tr></thead>
595+
<tbody>
596+
{% for ch in group.list %}
597+
<tr>
598+
<td>
599+
{% if ch.health == "good" %}<span class="badge badge-good">Gut</span>
600+
{% elif ch.health == "critical" %}<span class="badge badge-crit" title="{{ ch.health_detail }}">Kritisch</span>
601+
{% else %}<span class="badge badge-warn" title="{{ ch.health_detail }}">Warnung</span>{% endif %}
602+
</td>
603+
<td>{{ ch.channel_id }}</td>
604+
<td>{{ ch.frequency }}</td>
605+
<td class="{% if ch.power is not none %}{% if ch.power|abs > 10 %}val-crit{% elif ch.power|abs > 7 %}val-warn{% else %}val-good{% endif %}{% endif %}">{{ ch.power }}</td>
606+
<td class="{% if ch.snr is not none %}{% if ch.snr < 25 %}val-crit{% elif ch.snr < 30 %}val-warn{% else %}val-good{% endif %}{% endif %}">{{ ch.snr if ch.snr is not none else "-" }}</td>
607+
<td>{{ ch.modulation }}</td>
608+
<td>{{ "{:,}".format(ch.correctable_errors) }}</td>
609+
<td>{{ "{:,}".format(ch.uncorrectable_errors) }}</td>
610+
</tr>
611+
{% endfor %}
612+
</tbody>
613+
</table>
614+
</details>
615+
{% endfor %}
528616

529617
<h2 class="section-title">Upstream ({{ us|length }} Kanaele)</h2>
530-
<table>
531-
<thead><tr>
532-
<th>Status</th><th>Kanal</th><th>Frequenz</th><th>Power (dBmV)</th>
533-
<th>Modulation</th><th>Multiplex</th><th>DOCSIS</th>
534-
</tr></thead>
535-
<tbody>
536-
{% for ch in us %}
537-
<tr>
538-
<td>
539-
{% if ch.health == "good" %}<span class="badge badge-good">Gut</span>
540-
{% elif ch.health == "critical" %}<span class="badge badge-crit" title="{{ ch.health_detail }}">Kritisch</span>
541-
{% else %}<span class="badge badge-warn" title="{{ ch.health_detail }}">Warnung</span>{% endif %}
542-
</td>
543-
<td>{{ ch.channel_id }}</td>
544-
<td>{{ ch.frequency }}</td>
545-
<td class="{% if ch.power > 54 %}val-crit{% elif ch.power > 50 %}val-warn{% else %}val-good{% endif %}">{{ ch.power }}</td>
546-
<td>{{ ch.modulation }}</td>
547-
<td>{{ ch.multiplex }}</td>
548-
<td>{{ ch.docsis_version }}</td>
549-
</tr>
550-
{% endfor %}
551-
</tbody>
552-
</table>
553-
554-
<h2 class="section-title">Richtwerte</h2>
555-
<table class="ref-table">
556-
<thead><tr><th>Metrik</th><th>Gut</th><th>Grenzwertig</th><th>Schlecht</th></tr></thead>
557-
<tbody>
558-
<tr><td>DS Power</td><td>-7 .. +7 dBmV</td><td>&plusmn;7 .. &plusmn;10</td><td>&gt; &plusmn;10 dBmV</td></tr>
559-
<tr><td>US Power</td><td>35 .. 49 dBmV</td><td>50 .. 54</td><td>&gt; 54 dBmV</td></tr>
560-
<tr><td>SNR / MER</td><td>&gt; 30 dB</td><td>25 .. 30</td><td>&lt; 25 dB</td></tr>
561-
<tr><td>Unkorr. Fehler</td><td>niedrig</td><td>-</td><td>&gt; 10.000</td></tr>
562-
</tbody>
563-
</table>
618+
{% for group in us|sort(attribute='docsis_version', reverse=true)|groupby('docsis_version') %}
619+
<details class="channel-group" open>
620+
<summary>DOCSIS {{ group.grouper }} ({{ group.list|length }} Kanaele)</summary>
621+
<table>
622+
<thead><tr>
623+
<th>Status</th><th>Kanal</th><th>Frequenz</th><th>Power (dBmV)</th>
624+
<th>Modulation</th><th>Multiplex</th>
625+
</tr></thead>
626+
<tbody>
627+
{% for ch in group.list %}
628+
<tr>
629+
<td>
630+
{% if ch.health == "good" %}<span class="badge badge-good">Gut</span>
631+
{% elif ch.health == "critical" %}<span class="badge badge-crit" title="{{ ch.health_detail }}">Kritisch</span>
632+
{% else %}<span class="badge badge-warn" title="{{ ch.health_detail }}">Warnung</span>{% endif %}
633+
</td>
634+
<td>{{ ch.channel_id }}</td>
635+
<td>{{ ch.frequency }}</td>
636+
<td class="{% if ch.power > 54 %}val-crit{% elif ch.power > 50 %}val-warn{% else %}val-good{% endif %}">{{ ch.power }}</td>
637+
<td>{{ ch.modulation }}</td>
638+
<td>{{ ch.multiplex }}</td>
639+
</tr>
640+
{% endfor %}
641+
</tbody>
642+
</table>
643+
</details>
644+
{% endfor %}
564645

565646
{% elif not error %}
566647
<div class="waiting">

0 commit comments

Comments
 (0)