|
136 | 136 | border-right: 1px solid var(--input-border); |
137 | 137 | } |
138 | 138 | .sidebar.open { transform: translateX(0); } |
| 139 | + .sidebar-content { flex: 1; overflow-y: auto; } |
139 | 140 | .sidebar-header { |
140 | 141 | display: flex; |
141 | 142 | justify-content: space-between; |
|
306 | 307 | .val-good { color: var(--good); } |
307 | 308 | .val-warn { color: var(--warn); } |
308 | 309 | .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 | + } |
311 | 380 | .error-box { |
312 | 381 | background: rgba(244,67,54,0.15); |
313 | 382 | border: 1px solid var(--crit); |
|
393 | 462 | <span>Navigation</span> |
394 | 463 | <button class="sidebar-close" id="sidebar-close">×</button> |
395 | 464 | </div> |
396 | | - <a class="sidebar-link active" data-view="live"> |
397 | | - <span class="icon">●</span> Live Dashboard |
398 | | - </a> |
399 | | - <a class="sidebar-link" data-view="day"> |
400 | | - <span class="icon">⏱</span> Tagesverlauf |
401 | | - </a> |
402 | | - <a class="sidebar-link" data-view="week"> |
403 | | - <span class="icon">⚫</span> Wochentrend |
404 | | - </a> |
405 | | - <a class="sidebar-link" data-view="month"> |
406 | | - <span class="icon">📅</span> Monatstrend |
407 | | - </a> |
408 | | - <div class="sidebar-divider"></div> |
409 | | - <a class="sidebar-link" href="/settings"> |
410 | | - <span class="icon">⚙</span> Einstellungen |
411 | | - </a> |
| 465 | + <div class="sidebar-content"> |
| 466 | + <a class="sidebar-link active" data-view="live"> |
| 467 | + <span class="icon">●</span> Live Dashboard |
| 468 | + </a> |
| 469 | + <a class="sidebar-link" data-view="day"> |
| 470 | + <span class="icon">⏱</span> Tagesverlauf |
| 471 | + </a> |
| 472 | + <a class="sidebar-link" data-view="week"> |
| 473 | + <span class="icon">⚫</span> Wochentrend |
| 474 | + </a> |
| 475 | + <a class="sidebar-link" data-view="month"> |
| 476 | + <span class="icon">📅</span> Monatstrend |
| 477 | + </a> |
| 478 | + <div class="sidebar-divider"></div> |
| 479 | + <a class="sidebar-link" href="/settings"> |
| 480 | + <span class="icon">⚙</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>±7..10</td><td>>±10 dBmV</td></tr> |
| 489 | + <tr><td>US Power</td><td>35..49</td><td>50..54</td><td>>54 dBmV</td></tr> |
| 490 | + <tr><td>SNR/MER</td><td>>30 dB</td><td>25..30</td><td><25 dB</td></tr> |
| 491 | + <tr><td>Unkorr.</td><td>niedrig</td><td>-</td><td>>10.000</td></tr> |
| 492 | + </tbody> |
| 493 | + </table> |
| 494 | + </details> |
| 495 | + </div> |
412 | 496 | </nav> |
413 | 497 |
|
414 | 498 | <!-- Topbar --> |
|
500 | 584 | </div> |
501 | 585 |
|
502 | 586 | <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 %} |
528 | 616 |
|
529 | 617 | <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>±7 .. ±10</td><td>> ±10 dBmV</td></tr> |
559 | | - <tr><td>US Power</td><td>35 .. 49 dBmV</td><td>50 .. 54</td><td>> 54 dBmV</td></tr> |
560 | | - <tr><td>SNR / MER</td><td>> 30 dB</td><td>25 .. 30</td><td>< 25 dB</td></tr> |
561 | | - <tr><td>Unkorr. Fehler</td><td>niedrig</td><td>-</td><td>> 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 %} |
564 | 645 |
|
565 | 646 | {% elif not error %} |
566 | 647 | <div class="waiting"> |
|
0 commit comments