|
208 | 208 | /* ── Metric Cards Grid ── */ |
209 | 209 | .metric-cards { |
210 | 210 | display: grid; grid-template-columns: repeat(3, 1fr); |
211 | | - gap: 12px; margin-bottom: 16px; |
| 211 | + gap: 12px; margin-bottom: 16px; align-items: start; |
212 | 212 | } |
213 | 213 |
|
214 | 214 | /* ── Metric Card ── */ |
|
253 | 253 | .metric-unit { font-size: 0.7em; font-weight: normal; color: var(--muted); margin-left: 2px; } |
254 | 254 | .metric-label { font-size: 0.78em; color: var(--muted); margin-top: 2px; } |
255 | 255 |
|
256 | | - /* ── Gauge Bar ── */ |
| 256 | + /* ── Range Indicator ── */ |
| 257 | + .range-indicator { |
| 258 | + position: relative; height: 6px; border-radius: 3px; |
| 259 | + margin-top: 8px; display: flex; overflow: visible; |
| 260 | + } |
| 261 | + .range-zone { height: 100%; } |
| 262 | + .range-zone:first-child { border-radius: 3px 0 0 3px; } |
| 263 | + .range-zone:last-of-type { border-radius: 0 3px 3px 0; } |
| 264 | + .range-good { background: var(--good); opacity: 0.25; } |
| 265 | + .range-warn { background: var(--warn); opacity: 0.25; } |
| 266 | + .range-crit { background: var(--crit); opacity: 0.25; } |
| 267 | + .range-marker { |
| 268 | + position: absolute; top: -4px; width: 3px; height: 14px; |
| 269 | + border-radius: 1.5px; transform: translateX(-50%); |
| 270 | + transition: left 0.4s ease; |
| 271 | + } |
| 272 | + .range-marker.val-good { background: var(--good); } |
| 273 | + .range-marker.val-warn { background: var(--warn); } |
| 274 | + .range-marker.val-crit { background: var(--crit); } |
| 275 | + .range-labels { |
| 276 | + display: flex; justify-content: space-between; |
| 277 | + font-size: 0.6em; color: var(--muted); margin-top: 3px; |
| 278 | + } |
| 279 | + |
| 280 | + /* ── Gauge Bar (errors only) ── */ |
257 | 281 | .gauge-bar { |
258 | 282 | height: 4px; background: rgba(255,255,255,0.08); border-radius: 2px; |
259 | 283 | margin-top: 6px; overflow: hidden; |
|
287 | 311 | font-size: 0.78em; color: var(--muted); font-style: italic; |
288 | 312 | padding-bottom: 12px; |
289 | 313 | } |
| 314 | + .card-notice { |
| 315 | + font-size: 0.78em; padding: 6px 10px; margin-top: 8px; |
| 316 | + border-radius: 4px; display: flex; align-items: center; gap: 6px; |
| 317 | + } |
| 318 | + .card-notice-warn { |
| 319 | + background: rgba(255,152,0,0.1); color: var(--warn); |
| 320 | + } |
290 | 321 |
|
291 | 322 | /* ── Tables ── */ |
292 | 323 | table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 0.9em; } |
|
587 | 618 | else ('warn' if 'us_power_warn' in s.health_issues else 'good') %} |
588 | 619 | {% set error_health = 'crit' if 'uncorr_errors_high' in s.health_issues else 'good' %} |
589 | 620 |
|
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 | 621 | {# ── DS Power health ── #} |
596 | 622 | {% set ds_pwr_health = 'crit' if 'ds_power_critical' in s.health_issues else 'good' %} |
597 | 623 | {# ── SNR health ── #} |
598 | 624 | {% set snr_health = 'crit' if 'snr_critical' in s.health_issues else ('warn' if 'snr_warn' in s.health_issues else 'good') %} |
599 | 625 |
|
| 626 | + {# ── Range marker positions (clamped 2-98%) ── #} |
| 627 | + {% set ds_pwr_marker = [2, [98, ((s.ds_power_avg + 12) / 24 * 100)|round|int]|min]|max %} |
| 628 | + {% set snr_marker = [2, [98, ((s.ds_snr_avg - 15) / 30 * 100)|round|int]|min]|max %} |
| 629 | + {% set us_pwr_marker = [2, [98, ((s.us_power_avg - 25) / 35 * 100)|round|int]|min]|max %} |
| 630 | + |
600 | 631 | <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"> |
| 632 | + {# ── Card 1: Downstream ── #} |
| 633 | + <div class="metric-card health-{{ signal_health }}"> |
| 634 | + <div class="metric-card-header" onclick="toggleCard(this.parentNode)"> |
604 | 635 | <span class="status-dot"></span> |
605 | | - <span class="card-title">{{ t.card_signal_quality }}</span> |
| 636 | + <span class="card-title">↓ {{ t.card_signal_quality }}</span> |
606 | 637 | <span class="chevron">▶</span> |
607 | 638 | </div> |
608 | 639 | <div class="metric-card-body"> |
609 | 640 | <div class="metric-row"> |
610 | 641 | <div class="metric-item"> |
611 | 642 | <div class="metric-value val-{{ ds_pwr_health }}">{{ s.ds_power_avg }}<span class="metric-unit">dBmV</span></div> |
612 | 643 | <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> |
| 644 | + <div class="range-indicator"> |
| 645 | + <div class="range-zone range-crit" style="width:8.3%"></div> |
| 646 | + <div class="range-zone range-warn" style="width:12.5%"></div> |
| 647 | + <div class="range-zone range-good" style="width:58.3%"></div> |
| 648 | + <div class="range-zone range-warn" style="width:12.5%"></div> |
| 649 | + <div class="range-zone range-crit" style="width:8.4%"></div> |
| 650 | + <div class="range-marker val-{{ ds_pwr_health }}" style="left:{{ ds_pwr_marker }}%"></div> |
| 651 | + </div> |
| 652 | + <div class="range-labels"><span>-12</span><span>0</span><span>+12</span></div> |
614 | 653 | </div> |
615 | 654 | <div class="metric-item"> |
616 | 655 | <div class="metric-value val-{{ snr_health }}">{{ s.ds_snr_avg }}<span class="metric-unit">dB</span></div> |
617 | 656 | <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> |
| 657 | + <div class="range-indicator"> |
| 658 | + <div class="range-zone range-crit" style="width:33.3%"></div> |
| 659 | + <div class="range-zone range-warn" style="width:16.7%"></div> |
| 660 | + <div class="range-zone range-good" style="width:50%"></div> |
| 661 | + <div class="range-marker val-{{ snr_health }}" style="left:{{ snr_marker }}%"></div> |
| 662 | + </div> |
| 663 | + <div class="range-labels"><span>15</span><span>25</span><span>30</span><span>45 dB</span></div> |
619 | 664 | </div> |
620 | 665 | </div> |
621 | 666 | </div> |
|
630 | 675 | </div> |
631 | 676 |
|
632 | 677 | {# ── Card 2: Upstream ── #} |
633 | | - <div class="metric-card health-{{ us_health }}" onclick="toggleCard(this)"> |
634 | | - <div class="metric-card-header"> |
| 678 | + <div class="metric-card health-{{ us_health }}"> |
| 679 | + <div class="metric-card-header" onclick="toggleCard(this.parentNode)"> |
635 | 680 | <span class="status-dot"></span> |
636 | | - <span class="card-title">{{ t.card_upstream }}</span> |
| 681 | + <span class="card-title">↑ {{ t.card_upstream }}</span> |
637 | 682 | <span class="chevron">▶</span> |
638 | 683 | </div> |
639 | 684 | <div class="metric-card-body"> |
640 | 685 | <div class="metric-row"> |
641 | 686 | <div class="metric-item"> |
642 | 687 | <div class="metric-value val-{{ us_health }}">{{ s.us_power_avg }}<span class="metric-unit">dBmV</span></div> |
643 | 688 | <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> |
| 689 | + <div class="range-indicator"> |
| 690 | + <div class="range-zone range-good" style="width:68.6%"></div> |
| 691 | + <div class="range-zone range-warn" style="width:14.3%"></div> |
| 692 | + <div class="range-zone range-crit" style="width:17.1%"></div> |
| 693 | + <div class="range-marker val-{{ us_health }}" style="left:{{ us_pwr_marker }}%"></div> |
| 694 | + </div> |
| 695 | + <div class="range-labels"><span>25</span><span>35</span><span>49</span><span>54</span><span>60</span></div> |
645 | 696 | </div> |
646 | 697 | </div> |
| 698 | + {% if not has_us_ofdma %} |
| 699 | + <div class="card-notice card-notice-warn"> |
| 700 | + <span>⚠</span> {{ t.card_no_docsis31 }} |
| 701 | + </div> |
| 702 | + {% endif %} |
647 | 703 | </div> |
648 | 704 | <div class="metric-card-detail"> |
649 | 705 | <table class="detail-table"> |
|
655 | 711 | </div> |
656 | 712 |
|
657 | 713 | {# ── Card 3: Errors ── #} |
658 | | - <div class="metric-card health-{{ error_health }}" onclick="toggleCard(this)"> |
659 | | - <div class="metric-card-header"> |
| 714 | + <div class="metric-card health-{{ error_health }}"> |
| 715 | + <div class="metric-card-header" onclick="toggleCard(this.parentNode)"> |
660 | 716 | <span class="status-dot"></span> |
661 | 717 | <span class="card-title">{{ t.card_errors }}</span> |
662 | 718 | <span class="chevron">▶</span> |
|
0 commit comments