From 018b00d4cf3c3d2a5d94c443bc88083d9bbc1486 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 16:24:39 +0000 Subject: [PATCH 1/2] Handle dashboard promises and stop shadowing min/max builtins The dashboard called its async poll functions and main() as bare statements, dropping their promises: a rejection outside the functions' own try/catch (e.g. a render bug) surfaced only as an anonymous unhandled rejection. The visibilitychange refreshes are now explicitly fire-and-forget via void, and main() gets a .catch that logs the failure. A guard test fails if any of app.js's async functions is again called as a bare statement. handleHealthz and eventReaches named locals max and min, shadowing the Go 1.21 builtins; rename them to maxStale and minLevel. No behavior change. Resolves the six open SonarCloud findings (javascript:S9383, go:S978). Closes #168 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BvX6eSR4d9QW6L7vbDSVXK --- internal/alert/notify.go | 13 +++++++------ internal/httpapi/handlers.go | 2 +- internal/web/assets/app.js | 12 +++++++----- internal/web/polling_test.go | 28 ++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/internal/alert/notify.go b/internal/alert/notify.go index bf23fa5..38cbe66 100644 --- a/internal/alert/notify.go +++ b/internal/alert/notify.go @@ -414,14 +414,15 @@ func formatMessage(ev Event) string { ev.Metric, resource, ev.Kind, ev.From, ev.To, ev.Value) } -// eventReaches reports whether an event's severity reaches at least min. An -// event is relevant to a webhook when either side of the transition is at or -// above the webhook's minimum level, so a "min_level: crit" webhook still -// receives the cleared event when a metric drops out of crit. -func eventReaches(ev Event, min Level) bool { +// eventReaches reports whether an event's severity reaches at least +// minLevel. An event is relevant to a webhook when either side of the +// transition is at or above the webhook's minimum level, so a +// "min_level: crit" webhook still receives the cleared event when a metric +// drops out of crit. +func eventReaches(ev Event, minLevel Level) bool { peak := severity(ev.From) if s := severity(ev.To); s > peak { peak = s } - return peak >= severity(min) + return peak >= severity(minLevel) } diff --git a/internal/httpapi/handlers.go b/internal/httpapi/handlers.go index 1f5f93c..98dbc06 100644 --- a/internal/httpapi/handlers.go +++ b/internal/httpapi/handlers.go @@ -26,7 +26,7 @@ func writeJSON(w http.ResponseWriter, log func(msg string, args ...any), v any) // Config{} directly; production always sets it from // config.Config.HealthzMaxStaleness(...), which never returns zero. func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) { - if max := s.cfg.HealthzMaxStaleness; max > 0 && time.Since(s.metrics.Snapshot().Timestamp) > max { + if maxStale := s.cfg.HealthzMaxStaleness; maxStale > 0 && time.Since(s.metrics.Snapshot().Timestamp) > maxStale { http.Error(w, "stale: latest snapshot is older than the configured healthz_max_staleness_seconds", http.StatusServiceUnavailable) return } diff --git a/internal/web/assets/app.js b/internal/web/assets/app.js index 7f11b76..3b26f53 100644 --- a/internal/web/assets/app.js +++ b/internal/web/assets/app.js @@ -1197,10 +1197,12 @@ return; } // Refresh immediately on return so the user never looks at a stale - // card while waiting for the first interval to elapse. - pollMetrics(); - pollHistory(); - pollAlerts(); + // card while waiting for the first interval to elapse. The three + // refreshes are independent and handle their own fetch errors, so they + // are fired concurrently and deliberately not awaited. + void pollMetrics(); + void pollHistory(); + void pollAlerts(); startPolling(); }); } @@ -1228,5 +1230,5 @@ await reloadAll(); } - main(); + main().catch((e) => console.error('failed to initialise dashboard', e)); })(); diff --git a/internal/web/polling_test.go b/internal/web/polling_test.go index ef91ce8..1e5c9de 100644 --- a/internal/web/polling_test.go +++ b/internal/web/polling_test.go @@ -1,6 +1,7 @@ package web import ( + "regexp" "strings" "testing" ) @@ -61,3 +62,30 @@ func TestAppJS_PollFunctionsGuardInFlightRequests(t *testing.T) { } } } + +// TestAppJS_AsyncCallsAreNotFloating guards issue #168 (SonarCloud +// javascript:S9383): a call to one of app.js's own async functions written +// as a bare statement drops the returned promise, so any rejection surfaces +// only as an anonymous "unhandled rejection". Every such call must be +// awaited, explicitly discarded with `void` (fire-and-forget refreshes), or +// chained to a rejection handler (`main().catch(...)`) — all of which keep +// the call from starting the statement. +func TestAppJS_AsyncCallsAreNotFloating(t *testing.T) { + data, err := assetsFS.ReadFile("assets/app.js") + if err != nil { + t.Fatalf("read app.js: %v", err) + } + js := string(data) + + decls := regexp.MustCompile(`async function (\w+)\(`).FindAllStringSubmatch(js, -1) + if len(decls) == 0 { + t.Fatal("app.js: expected at least one async function declaration") + } + for _, d := range decls { + bare := regexp.MustCompile(`(?m)^[ \t]*` + d[1] + `\([^)]*\);`) + for _, loc := range bare.FindAllStringIndex(js, -1) { + line := strings.Count(js[:loc[0]], "\n") + 1 + t.Errorf("app.js:%d: %s() is called as a bare statement; await it, prefix it with void, or attach .catch", line, d[1]) + } + } +} From 7b454ff0ae99f8335300e74fdcb9923fd998c748 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 16:30:27 +0000 Subject: [PATCH 2/2] Bump Go toolchain to 1.26.9 for stdlib security fixes govulncheck reports ten reachable standard-library vulnerabilities in crypto/tls, net/http and net/textproto (GO-2026-6603 through GO-2026-6617) for go1.26.7, the version go.mod pinned. All are fixed in go1.26.9. This was failing the Vulnerability scan job on main as well. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BvX6eSR4d9QW6L7vbDSVXK --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 65f1a87..c4ead91 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/larslaskowski/pimonitor -go 1.26.7 +go 1.26.9 require gopkg.in/yaml.v3 v3.0.1