diff --git a/main.go b/main.go index a0aa3ab..134c5f5 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,8 @@ type store struct { history []sample } +const maxHistoryPoints = 10000 + type sample struct { at time.Time value int @@ -42,6 +44,11 @@ func (s *store) set(v int) { now := time.Now() s.updatedAt = now s.history = append(s.history, sample{at: now, value: v}) + s.trimHistoryLocked(now) + s.mu.Unlock() +} + +func (s *store) trimHistoryLocked(now time.Time) { cutoff := now.Add(-12 * time.Hour) keep := s.history[:0] for _, point := range s.history { @@ -49,8 +56,10 @@ func (s *store) set(v int) { keep = append(keep, point) } } + if len(keep) > maxHistoryPoints { + keep = keep[len(keep)-maxHistoryPoints:] + } s.history = keep - s.mu.Unlock() } func (s *store) snapshot() (int, time.Time, []sample) { @@ -63,6 +72,7 @@ func (s *store) snapshot() (int, time.Time, []sample) { func main() { secret := os.Getenv("WRITE_SECRET") addr := os.Getenv("ADDR") + chartMax := 0 if cfg, err := loadConfig("config.json"); err != nil { if !errors.Is(err, os.ErrNotExist) { log.Fatalf("load config: %v", err) @@ -74,6 +84,9 @@ func main() { if addr == "" { addr = cfg.Addr } + if cfg.ChartMaxWatts > 0 { + chartMax = cfg.ChartMaxWatts + } } if secret == "" { log.Fatal("WRITE_SECRET is required") @@ -83,8 +96,9 @@ func main() { } app := &server{ - secret: secret, - store: &store{}, + secret: secret, + chartMax: chartMax, + store: &store{}, } log.Printf("listening on %s", addr) @@ -92,13 +106,15 @@ func main() { } type server struct { - secret string - store *store + secret string + chartMax int + store *store } type config struct { - WriteSecret string `json:"write_secret"` - Addr string `json:"addr"` + WriteSecret string `json:"write_secret"` + Addr string `json:"addr"` + ChartMaxWatts int `json:"chart_max_watts"` } func loadConfig(path string) (config, error) { @@ -143,17 +159,17 @@ func (s *server) handleIndex(w http.ResponseWriter, r *http.Request) { if err := page.Execute(w, map[string]any{ "Watts": watts, "UpdatedAt": updatedLabel, - "HistoryChart": buildHistoryChart(history), + "HistoryChart": buildHistoryChart(history, s.chartMax), }); err != nil { http.Error(w, "render error", http.StatusInternalServerError) } } -func buildHistoryChart(history []sample) template.HTML { +func buildHistoryChart(history []sample, maxWatts int) template.HTML { const ( width = 640 height = 220 - padL = 52 + padL = 72 padR = 18 padT = 18 padB = 42 @@ -164,10 +180,13 @@ func buildHistoryChart(history []sample) template.HTML { } minV, maxV := 0, history[0].value + if maxWatts > 0 { + maxV = maxWatts + } start := history[0].at end := history[len(history)-1].at for _, point := range history[1:] { - if point.value > maxV { + if maxWatts <= 0 && point.value > maxV { maxV = point.value } if point.at.Before(start) { @@ -193,6 +212,12 @@ func buildHistoryChart(history []sample) template.HTML { for _, point := range history { x := float64(padL) + plotW*point.at.Sub(start).Seconds()/span.Seconds() y := float64(height-padB) - plotH*(float64(point.value-minV)/float64(maxV-minV)) + if y < float64(padT) { + y = float64(padT) + } + if y > float64(height-padB) { + y = float64(height - padB) + } points = append(points, fmt.Sprintf("%.1f,%.1f", x, y)) lastX, lastY = x, y } diff --git a/main_test.go b/main_test.go index 7eaddb1..a8f4193 100644 --- a/main_test.go +++ b/main_test.go @@ -66,7 +66,7 @@ func TestHandleWriteStoresValue(t *testing.T) { func TestLoadConfig(t *testing.T) { path := t.TempDir() + "/config.json" - if err := os.WriteFile(path, []byte(`{"write_secret":"secret","addr":":9090"}`), 0o600); err != nil { + if err := os.WriteFile(path, []byte(`{"write_secret":"secret","addr":":9090","chart_max_watts":750}`), 0o600); err != nil { t.Fatalf("write config: %v", err) } @@ -80,10 +80,13 @@ func TestLoadConfig(t *testing.T) { if cfg.Addr != ":9090" { t.Fatalf("addr = %q, want %q", cfg.Addr, ":9090") } + if cfg.ChartMaxWatts != 750 { + t.Fatalf("chart_max_watts = %d, want %d", cfg.ChartMaxWatts, 750) + } } func TestBuildHistoryChartEmpty(t *testing.T) { - chart := buildHistoryChart(nil) + chart := buildHistoryChart(nil, 0) if !strings.Contains(string(chart), "No power history yet") { t.Fatalf("empty chart did not contain fallback text: %s", string(chart)) } @@ -96,7 +99,7 @@ func TestBuildHistoryChartRendersPolyline(t *testing.T) { {at: time.Date(2026, 4, 20, 12, 0, 0, 0, time.UTC), value: 150}, } - chart := buildHistoryChart(history) + chart := buildHistoryChart(history, 0) if !strings.Contains(string(chart), "