Skip to content

Commit dfa5331

Browse files
feat: implement duration picker for data retrieval and enhance GetData functionality
1 parent c8fe34a commit dfa5331

3 files changed

Lines changed: 186 additions & 24 deletions

File tree

internal/server/handlers.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package server
22

33
import (
44
"net/http"
5+
"strconv"
56
"time"
67

78
"github.com/labstack/echo/v5"
@@ -28,6 +29,13 @@ type DashboardData struct {
2829
}
2930

3031
func (h *Handler) GetData(c *echo.Context) error {
32+
minutes := 60
33+
if m := c.QueryParam("minutes"); m != "" {
34+
if n, err := strconv.Atoi(m); err == nil && n > 0 && n <= 43200 {
35+
minutes = n
36+
}
37+
}
38+
3139
summary, err := h.store.GetSummary()
3240
if err != nil {
3341
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -40,7 +48,7 @@ func (h *Handler) GetData(c *echo.Context) error {
4048
if err != nil {
4149
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
4250
}
43-
history, err := h.store.GetHistory(30)
51+
history, err := h.store.GetHistoryWindow(minutes)
4452
if err != nil {
4553
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
4654
}

internal/store/store.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,45 @@ func (s *Store) GetHistory(limit int) ([]Measurement, error) {
150150
return results, nil
151151
}
152152

153+
func (s *Store) GetHistoryWindow(minutes int) ([]Measurement, error) {
154+
cutoff := time.Now().Add(-time.Duration(minutes) * time.Minute).Format(time.RFC3339)
155+
rows, err := s.db.Query(
156+
`SELECT id, ts, network_id, latency, jitter, packet_loss, download, upload, dns
157+
FROM measurements WHERE ts >= ? ORDER BY ts ASC`, cutoff,
158+
)
159+
if err != nil {
160+
return nil, err
161+
}
162+
defer rows.Close()
163+
164+
var timeFmt string
165+
switch {
166+
case minutes > 1440:
167+
timeFmt = "Jan 02 15:04"
168+
case minutes > 60:
169+
timeFmt = "15:04"
170+
default:
171+
timeFmt = "15:04:05"
172+
}
173+
174+
var results []Measurement
175+
for rows.Next() {
176+
var m Measurement
177+
var ts string
178+
if err := rows.Scan(&m.ID, &ts, &m.NetworkID, &m.Latency, &m.Jitter, &m.PacketLoss, &m.Download, &m.Upload, &m.DNS); err != nil {
179+
return nil, err
180+
}
181+
t, _ := time.Parse(time.RFC3339, ts)
182+
m.Time = t.Format(timeFmt)
183+
results = append(results, m)
184+
}
185+
186+
if results == nil {
187+
results = []Measurement{}
188+
}
189+
return results, nil
190+
}
191+
153192
// --- Summary ---
154193

155194
type Summary struct {

web/index.html

Lines changed: 138 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,60 @@
158158
50% { opacity: 0.4; }
159159
}
160160

161-
.select-input {
161+
.duration-btn {
162162
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; }
168215

169216
.theme-toggle {
170217
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>
421468
</div>
422469
</div>
423470
<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>
429497
<button class="theme-toggle icon-btn" id="themeToggle" title="Toggle dark mode">
430498
<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>
431499
<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">
524592
</div>
525593

526594
<!-- 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">
528596

529597
<!-- Left column: metrics + charts -->
530-
<div class="flex flex-col gap-4 min-h-0">
598+
<div class="flex flex-col gap-4">
531599

532600
<!-- Summary metric cards -->
533601
<div id="summaryCards" class="grid grid-cols-2 sm:grid-cols-4 xl:grid-cols-4 gap-3 flex-shrink-0"></div>
534602

535603
<!-- 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">
538606
<div class="chart-header">
539607
<h3><span class="chart-dot" style="background:var(--accent)"></span> Latency</h3>
540608
<span class="text-xs font-medium" style="color:var(--muted)" id="latencyAvgLabel"></span>
541609
</div>
542-
<div id="latencyChart" class="flex-1"></div>
610+
<div id="latencyChart"></div>
543611
</div>
544-
<div class="card chart-box flex flex-col">
612+
<div class="card chart-box">
545613
<div class="chart-header">
546614
<h3><span class="chart-dot" style="background:var(--green)"></span> Throughput</h3>
547615
<span class="text-xs font-medium" style="color:var(--muted)" id="speedAvgLabel"></span>
548616
</div>
549-
<div id="speedChart" class="flex-1"></div>
617+
<div id="speedChart"></div>
550618
</div>
551-
<div class="card chart-box flex flex-col">
619+
<div class="card chart-box">
552620
<div class="chart-header">
553621
<h3><span class="chart-dot" style="background:var(--red)"></span> Packet Loss &amp; Jitter</h3>
554622
<span class="text-xs font-medium" style="color:var(--muted)" id="lossAvgLabel"></span>
555623
</div>
556-
<div id="lossChart" class="flex-1"></div>
624+
<div id="lossChart"></div>
557625
</div>
558-
<div class="card chart-box flex flex-col">
626+
<div class="card chart-box">
559627
<div class="chart-header">
560628
<h3><span class="chart-dot" style="background:var(--purple)"></span> DNS Resolution</h3>
561629
<span class="text-xs font-medium" style="color:var(--muted)" id="dnsAvgLabel"></span>
562630
</div>
563-
<div id="dnsChart" class="flex-1"></div>
631+
<div id="dnsChart"></div>
564632
</div>
565633
</div>
566634
</div>
@@ -682,6 +750,7 @@ <h3><span class="chart-dot" style="background:var(--purple)"></span> DNS Resolut
682750
let chartInstances = {};
683751
let lastUpdateTime = null;
684752
let isLoading = false;
753+
let currentMinutes = 60;
685754

686755
// ── Live "Xs ago" ticker ──────────────────────────────────────────────
687756
setInterval(() => {
@@ -712,7 +781,7 @@ <h3><span class="chart-dot" style="background:var(--purple)"></span> DNS Resolut
712781
if (lu) { lu.classList.add("refreshing"); lu.textContent = "Refreshing…"; }
713782

714783
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),
716785
fetch("/api/config").then(r => r.json()).catch(() => null),
717786
]);
718787

@@ -933,6 +1002,52 @@ <h3><span class="chart-dot" style="background:var(--purple)"></span> DNS Resolut
9331002
$("#qs-jitter").textContent = s.jitter_avg + " ms";
9341003
}
9351004

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+
9361051
// ── Theme toggle ──────────────────────────────────────────────────────
9371052
const toggle = $("#themeToggle");
9381053
function applyTheme(dark) {

0 commit comments

Comments
 (0)