Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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

Expand Down
13 changes: 7 additions & 6 deletions internal/alert/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion internal/httpapi/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 7 additions & 5 deletions internal/web/assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}
Expand Down Expand Up @@ -1228,5 +1230,5 @@
await reloadAll();
}

main();
main().catch((e) => console.error('failed to initialise dashboard', e));
})();
28 changes: 28 additions & 0 deletions internal/web/polling_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package web

import (
"regexp"
"strings"
"testing"
)
Expand Down Expand Up @@ -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])
}
}
}
Loading