|
197 | 197 | .health-issue-name { font-weight: bold; font-size: 0.9em; display: block; } |
198 | 198 | .health-issue-desc { font-size: 0.82em; color: var(--muted); margin-top: 2px; display: block; } |
199 | 199 | 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; |
203 | 289 | } |
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; } |
207 | 290 |
|
208 | 291 | /* ── Tables ── */ |
209 | 292 | table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 0.9em; } |
|
325 | 408 | .sidebar { width: 0; min-width: 0; } |
326 | 409 | .sidebar.collapsed { width: 0; min-width: 0; } |
327 | 410 | } |
| 411 | + @media (max-width: 900px) { |
| 412 | + .metric-cards { grid-template-columns: 1fr; } |
| 413 | + } |
328 | 414 | @media (max-width: 600px) { |
329 | 415 | body { font-size: 14px; } |
330 | 416 | .topbar { gap: 6px; padding: 8px 10px; } |
331 | 417 | .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; } |
333 | 420 | table { font-size: 0.8em; } |
334 | 421 | th, td { padding: 4px 6px; } |
335 | 422 | .calendar-popup { left: 10px; right: 10px; min-width: auto; } |
|
480 | 567 | {% endif %} |
481 | 568 | </div> |
482 | 569 |
|
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">·</span>{% endif %} |
| 576 | + {{ connection_info.max_downstream_kbps // 1000 }}/{{ connection_info.max_upstream_kbps // 1000 }} Mbit/s |
489 | 577 | {% 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">▶</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 >30 dB</td></tr> |
| 628 | + </table> |
| 629 | + </div> |
505 | 630 | </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">▶</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> |
509 | 655 | </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">▶</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> |
513 | 680 | </div> |
514 | 681 | </div> |
515 | 682 |
|
@@ -1058,6 +1225,9 @@ <h2>{{ t.export_title }}</h2> |
1058 | 1225 | setTimeout(function() { btn.textContent = T.copy_clipboard; }, 3000); |
1059 | 1226 | } |
1060 | 1227 | } |
| 1228 | +function toggleCard(el) { |
| 1229 | + el.classList.toggle('open'); |
| 1230 | +} |
1061 | 1231 | </script> |
1062 | 1232 |
|
1063 | 1233 | </body> |
|
0 commit comments