|
158 | 158 | 50% { opacity: 0.4; } |
159 | 159 | } |
160 | 160 |
|
161 | | - .select-input { |
| 161 | + .duration-btn { |
162 | 162 | background: var(--card); border: 1px solid var(--border); border-radius: 8px; |
163 | | - padding: 0.5rem 0.75rem; color: var(--fg); font-size: 0.8125rem; |
164 | | - font-weight: 500; cursor: pointer; transition: all 0.15s ease; |
165 | | - } |
166 | | - .select-input:hover { border-color: var(--border-hover); background: var(--input-hover-bg); } |
167 | | - .select-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(59,130,246,0.12); } |
| 163 | + height: 36px; padding: 0 10px; cursor: pointer; |
| 164 | + display: flex; align-items: center; gap: 5px; transition: all 0.2s ease; |
| 165 | + color: var(--subtle); white-space: nowrap; |
| 166 | + } |
| 167 | + .duration-btn:hover { border-color: var(--border-hover); background: var(--input-hover-bg); color: var(--fg); } |
| 168 | + .duration-btn .dur-chevron { transition: transform 0.2s ease; flex-shrink: 0; } |
| 169 | + .duration-btn.open .dur-chevron { transform: rotate(180deg); } |
| 170 | + .duration-label { font-size: 0.8125rem; font-weight: 600; color: var(--fg); } |
| 171 | + |
| 172 | + .duration-wrapper { position: relative; } |
| 173 | + .duration-panel { |
| 174 | + position: absolute; right: 0; top: calc(100% + 6px); |
| 175 | + background: var(--card); border: 1px solid var(--border); border-radius: 12px; |
| 176 | + padding: 10px; box-shadow: 0 8px 32px var(--shadow-hover); width: 190px; z-index: 200; |
| 177 | + opacity: 0; transform: translateY(-6px) scale(0.98); pointer-events: none; |
| 178 | + transition: opacity 0.18s ease, transform 0.18s ease; |
| 179 | + } |
| 180 | + .duration-panel.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: all; } |
| 181 | + |
| 182 | + .dur-presets { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; margin-bottom: 8px; } |
| 183 | + .dur-chip { |
| 184 | + background: transparent; border: 1px solid var(--border); border-radius: 6px; |
| 185 | + padding: 5px 2px; font-size: 0.75rem; font-weight: 600; color: var(--subtle); |
| 186 | + cursor: pointer; transition: all 0.15s; text-align: center; font-family: 'Inter', sans-serif; |
| 187 | + } |
| 188 | + .dur-chip:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); } |
| 189 | + .dur-chip.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); } |
| 190 | + |
| 191 | + .dur-divider { |
| 192 | + font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; |
| 193 | + color: var(--muted); margin: 4px 0 8px; padding-top: 8px; border-top: 1px solid var(--border); |
| 194 | + } |
| 195 | + .dur-custom { display: flex; align-items: center; gap: 5px; } |
| 196 | + .dur-custom-label { font-size: 0.8125rem; color: var(--subtle); flex-shrink: 0; } |
| 197 | + .dur-num { |
| 198 | + width: 50px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; |
| 199 | + padding: 4px 6px; color: var(--fg); font-size: 0.8125rem; font-weight: 500; |
| 200 | + font-family: 'Inter', sans-serif; text-align: center; transition: border-color 0.15s; |
| 201 | + } |
| 202 | + .dur-num:focus { outline: none; border-color: var(--accent); } |
| 203 | + .dur-unit { |
| 204 | + flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; |
| 205 | + padding: 4px 5px; color: var(--fg); font-size: 0.8125rem; font-weight: 500; |
| 206 | + font-family: 'Inter', sans-serif; cursor: pointer; transition: border-color 0.15s; |
| 207 | + } |
| 208 | + .dur-unit:focus { outline: none; border-color: var(--accent); } |
| 209 | + .dur-apply { |
| 210 | + width: 100%; margin-top: 8px; padding: 6px; border-radius: 7px; |
| 211 | + background: var(--accent); color: #fff; border: none; font-size: 0.8125rem; font-weight: 600; |
| 212 | + cursor: pointer; transition: opacity 0.15s; font-family: 'Inter', sans-serif; |
| 213 | + } |
| 214 | + .dur-apply:hover { opacity: 0.85; } |
168 | 215 |
|
169 | 216 | .theme-toggle { |
170 | 217 | background: var(--card); border: 1px solid var(--border); border-radius: 8px; |
@@ -421,11 +468,32 @@ <h1 class="text-xl sm:text-2xl font-extrabold tracking-tight">netmon</h1> |
421 | 468 | </div> |
422 | 469 | </div> |
423 | 470 | <div class="flex gap-2 items-center"> |
424 | | - <select class="select-input" id="timeRange"> |
425 | | - <option>Last 15 min</option> |
426 | | - <option>Last 1 hour</option> |
427 | | - <option>Last 24 hours</option> |
428 | | - </select> |
| 471 | + <div class="duration-wrapper" id="durationWrapper"> |
| 472 | + <button class="duration-btn" id="durationBtn" title="Time range"> |
| 473 | + <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> |
| 474 | + <span class="duration-label" id="durationLabel">1h</span> |
| 475 | + <svg class="dur-chevron" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg> |
| 476 | + </button> |
| 477 | + <div class="duration-panel" id="durationPanel"> |
| 478 | + <div class="dur-presets"> |
| 479 | + <button class="dur-chip" data-minutes="15" data-label="15m">15m</button> |
| 480 | + <button class="dur-chip active" data-minutes="60" data-label="1h">1h</button> |
| 481 | + <button class="dur-chip" data-minutes="360" data-label="6h">6h</button> |
| 482 | + <button class="dur-chip" data-minutes="1440" data-label="24h">24h</button> |
| 483 | + </div> |
| 484 | + <div class="dur-divider">Custom</div> |
| 485 | + <div class="dur-custom"> |
| 486 | + <span class="dur-custom-label">Last</span> |
| 487 | + <input class="dur-num" type="number" id="durNum" min="1" max="9999" value="1"> |
| 488 | + <select class="dur-unit" id="durUnit"> |
| 489 | + <option value="1">min</option> |
| 490 | + <option value="60" selected>hr</option> |
| 491 | + <option value="1440">day</option> |
| 492 | + </select> |
| 493 | + </div> |
| 494 | + <button class="dur-apply" id="durApply">Apply</button> |
| 495 | + </div> |
| 496 | + </div> |
429 | 497 | <button class="theme-toggle icon-btn" id="themeToggle" title="Toggle dark mode"> |
430 | 498 | <svg id="iconMoon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg> |
431 | 499 | <svg id="iconSun" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg> |
@@ -524,43 +592,43 @@ <h2 id="modalTitle"> |
524 | 592 | </div> |
525 | 593 |
|
526 | 594 | <!-- Main grid --> |
527 | | - <div class="flex-1 grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-4 min-h-0 pb-4 lg:pb-0"> |
| 595 | + <div class="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-4 pb-4 lg:pb-0"> |
528 | 596 |
|
529 | 597 | <!-- Left column: metrics + charts --> |
530 | | - <div class="flex flex-col gap-4 min-h-0"> |
| 598 | + <div class="flex flex-col gap-4"> |
531 | 599 |
|
532 | 600 | <!-- Summary metric cards --> |
533 | 601 | <div id="summaryCards" class="grid grid-cols-2 sm:grid-cols-4 xl:grid-cols-4 gap-3 flex-shrink-0"></div> |
534 | 602 |
|
535 | 603 | <!-- Charts 2x2 --> |
536 | | - <div class="grid grid-cols-1 md:grid-cols-2 gap-3 flex-1 min-h-0"> |
537 | | - <div class="card chart-box flex flex-col"> |
| 604 | + <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> |
| 605 | + <div class="card chart-box"> |
538 | 606 | <div class="chart-header"> |
539 | 607 | <h3><span class="chart-dot" style="background:var(--accent)"></span> Latency</h3> |
540 | 608 | <span class="text-xs font-medium" style="color:var(--muted)" id="latencyAvgLabel">—</span> |
541 | 609 | </div> |
542 | | - <div id="latencyChart" class="flex-1"></div> |
| 610 | + <div id="latencyChart"></div> |
543 | 611 | </div> |
544 | | - <div class="card chart-box flex flex-col"> |
| 612 | + <div class="card chart-box"> |
545 | 613 | <div class="chart-header"> |
546 | 614 | <h3><span class="chart-dot" style="background:var(--green)"></span> Throughput</h3> |
547 | 615 | <span class="text-xs font-medium" style="color:var(--muted)" id="speedAvgLabel">—</span> |
548 | 616 | </div> |
549 | | - <div id="speedChart" class="flex-1"></div> |
| 617 | + <div id="speedChart"></div> |
550 | 618 | </div> |
551 | | - <div class="card chart-box flex flex-col"> |
| 619 | + <div class="card chart-box"> |
552 | 620 | <div class="chart-header"> |
553 | 621 | <h3><span class="chart-dot" style="background:var(--red)"></span> Packet Loss & Jitter</h3> |
554 | 622 | <span class="text-xs font-medium" style="color:var(--muted)" id="lossAvgLabel">—</span> |
555 | 623 | </div> |
556 | | - <div id="lossChart" class="flex-1"></div> |
| 624 | + <div id="lossChart"></div> |
557 | 625 | </div> |
558 | | - <div class="card chart-box flex flex-col"> |
| 626 | + <div class="card chart-box"> |
559 | 627 | <div class="chart-header"> |
560 | 628 | <h3><span class="chart-dot" style="background:var(--purple)"></span> DNS Resolution</h3> |
561 | 629 | <span class="text-xs font-medium" style="color:var(--muted)" id="dnsAvgLabel">—</span> |
562 | 630 | </div> |
563 | | - <div id="dnsChart" class="flex-1"></div> |
| 631 | + <div id="dnsChart"></div> |
564 | 632 | </div> |
565 | 633 | </div> |
566 | 634 | </div> |
@@ -682,6 +750,7 @@ <h3><span class="chart-dot" style="background:var(--purple)"></span> DNS Resolut |
682 | 750 | let chartInstances = {}; |
683 | 751 | let lastUpdateTime = null; |
684 | 752 | let isLoading = false; |
| 753 | + let currentMinutes = 60; |
685 | 754 |
|
686 | 755 | // ── Live "Xs ago" ticker ────────────────────────────────────────────── |
687 | 756 | setInterval(() => { |
@@ -712,7 +781,7 @@ <h3><span class="chart-dot" style="background:var(--purple)"></span> DNS Resolut |
712 | 781 | if (lu) { lu.classList.add("refreshing"); lu.textContent = "Refreshing…"; } |
713 | 782 |
|
714 | 783 | const [data, cfg] = await Promise.all([ |
715 | | - fetch("/api/data").then(r => r.json()).catch(() => null), |
| 784 | + fetch(`/api/data?minutes=${currentMinutes}`).then(r => r.json()).catch(() => null), |
716 | 785 | fetch("/api/config").then(r => r.json()).catch(() => null), |
717 | 786 | ]); |
718 | 787 |
|
@@ -933,6 +1002,52 @@ <h3><span class="chart-dot" style="background:var(--purple)"></span> DNS Resolut |
933 | 1002 | $("#qs-jitter").textContent = s.jitter_avg + " ms"; |
934 | 1003 | } |
935 | 1004 |
|
| 1005 | + // ── Duration picker ─────────────────────────────────────────────────── |
| 1006 | + const durationBtn = $("#durationBtn"); |
| 1007 | + const durationPanel = $("#durationPanel"); |
| 1008 | + const durationLabel = $("#durationLabel"); |
| 1009 | + |
| 1010 | + function setDuration(minutes, label, reload = true) { |
| 1011 | + currentMinutes = minutes; |
| 1012 | + durationLabel.textContent = label; |
| 1013 | + document.querySelectorAll(".dur-chip").forEach(c => { |
| 1014 | + c.classList.toggle("active", parseInt(c.dataset.minutes) === minutes); |
| 1015 | + }); |
| 1016 | + closeDurationPanel(); |
| 1017 | + if (reload) loadData(true); |
| 1018 | + } |
| 1019 | + |
| 1020 | + function closeDurationPanel() { |
| 1021 | + durationPanel.classList.remove("open"); |
| 1022 | + durationBtn.classList.remove("open"); |
| 1023 | + } |
| 1024 | + |
| 1025 | + durationBtn.addEventListener("click", e => { |
| 1026 | + e.stopPropagation(); |
| 1027 | + const isOpen = durationPanel.classList.toggle("open"); |
| 1028 | + durationBtn.classList.toggle("open", isOpen); |
| 1029 | + }); |
| 1030 | + |
| 1031 | + document.querySelectorAll(".dur-chip").forEach(chip => { |
| 1032 | + chip.addEventListener("click", () => { |
| 1033 | + setDuration(parseInt(chip.dataset.minutes), chip.dataset.label); |
| 1034 | + }); |
| 1035 | + }); |
| 1036 | + |
| 1037 | + $("#durApply").addEventListener("click", () => { |
| 1038 | + const n = Math.max(1, parseInt($("#durNum").value) || 1); |
| 1039 | + const unit = parseInt($("#durUnit").value); |
| 1040 | + const mins = n * unit; |
| 1041 | + const unitLabel = unit === 1 ? "m" : unit === 60 ? "h" : "d"; |
| 1042 | + setDuration(mins, n + unitLabel); |
| 1043 | + }); |
| 1044 | + |
| 1045 | + $("#durNum").addEventListener("keydown", e => { if (e.key === "Enter") $("#durApply").click(); }); |
| 1046 | + |
| 1047 | + document.addEventListener("click", e => { |
| 1048 | + if (!$("#durationWrapper").contains(e.target)) closeDurationPanel(); |
| 1049 | + }); |
| 1050 | + |
936 | 1051 | // ── Theme toggle ────────────────────────────────────────────────────── |
937 | 1052 | const toggle = $("#themeToggle"); |
938 | 1053 | function applyTheme(dark) { |
|
0 commit comments