diff --git a/VERSION b/VERSION index 0d91a54..1c09c74 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.3.3 diff --git a/cmd/ts3monolith/main.go b/cmd/ts3monolith/main.go index bbd585c..36f258a 100644 --- a/cmd/ts3monolith/main.go +++ b/cmd/ts3monolith/main.go @@ -52,15 +52,31 @@ func main() { func collect(ctx context.Context, cfg config.Config, tq *ts3.Query, rm *rrd.Manager) { run := func() { c, cancel := context.WithTimeout(ctx, cfg.TSQueryTimeout) - defer cancel() n, err := tq.CountOnline(c) + cancel() if err != nil { log.Printf("stats query failed: %v", err) - return } - c2, cancel2 := context.WithTimeout(ctx, 5*time.Second) - defer cancel2() - if err := rm.Update(c2, n); err != nil { + // Treat "query failed" and "query succeeded but nobody is online" + // the same way: repeat the last known sample instead of writing 0. + // This matches the original PHP app's collector exactly (it never + // distinguished the two cases either). At a 1-minute sampling + // interval a real but brief empty-server moment plots as a sharp + // spike down to 0 and back up, which reads as broken/jagged far + // more than it reads as useful information -- so, deliberately, + // a true "server went empty for a while" no longer shows as 0 in + // the graph, same trade-off the original app made. + if n == 0 { + c2, cancel2 := context.WithTimeout(ctx, 2*time.Second) + last, lastErr := rm.Last(c2) + cancel2() + if lastErr == nil { + n = last + } + } + c3, cancel3 := context.WithTimeout(ctx, 5*time.Second) + defer cancel3() + if err := rm.Update(c3, n); err != nil { log.Printf("rrd update failed: %v", err) } } diff --git a/internal/rrd/rrd.go b/internal/rrd/rrd.go index 7673230..3ec0c4a 100644 --- a/internal/rrd/rrd.go +++ b/internal/rrd/rrd.go @@ -134,6 +134,14 @@ func (m *Manager) Graph(ctx context.Context, key, theme string) (string, error) "graph", out, "--imgformat", "PNG", "--start", rg.Start, "--end", "now", + // By default rrdtool's --width/--height size only the plot canvas; + // the title, legend rows and axis-label margins then pad the final + // PNG to something larger and variable (depends on title/legend + // text length). --full-size-mode makes --width/--height the exact + // *total* image size instead, so the PNG is always precisely + // 1200x320 -- matching the CSS aspect-ratio on the exactly, + // with no letterboxing from a size mismatch. + "--full-size-mode", "--width", "1200", "--height", "320", "--lower-limit", "0", "--alt-autoscale-max", "--rigid", "--slope-mode", diff --git a/internal/web/assets/app.css b/internal/web/assets/app.css index 4a13595..e2afe64 100644 --- a/internal/web/assets/app.css +++ b/internal/web/assets/app.css @@ -106,7 +106,7 @@ html[data-theme=light] .theme-toggle .ic-moon{display:block} .graphs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px} .graph-image-button{display:block;width:100%;padding:0;border:0;background:var(--field);cursor:zoom-in;overflow:hidden} -.graph-image-button img{display:block;width:100%;height:auto;aspect-ratio:3.56/1;object-fit:contain;transition:transform .18s ease,opacity .18s ease} +.graph-image-button img{display:block;width:100%;height:auto;aspect-ratio:1200/320;object-fit:contain;transition:transform .18s ease,opacity .18s ease} .graph-image-button:hover img{transform:scale(1.012);opacity:.96} .lightbox[hidden]{display:none}