fix issues TUI display reliability
This commit is contained in:
1 parent
a96e95212a
commit
f5015581a4
5 files changed
+84
-5
No files matched your search
+16
-3
@@ -155,6 +155,10 @@ func spikeKey(s features.Spike) string {
|
||||
return s.Service + "\x00" + strings.Join(sigs, "\x00")
|
||||
}
|
||||
|
||||
func patternKey(service, signature string) string {
|
||||
return service + "\x00" + signature
|
||||
}
|
||||
|
||||
// Scan runs one evaluation, mirroring the runner's Tick: load the window
|
||||
// ending lag ago plus its baseline, detect exactly what the server pages on,
|
||||
// and use a cooldown suppressor to tell a fresh page from an ongoing one.
|
||||
@@ -184,9 +188,11 @@ func (s *scanner) Scan(ctx context.Context, now time.Time) snapshot {
|
||||
// The suppressor mutates only its own state and the copies Filter makes,
|
||||
// so firing keeps every pattern for display while paged marks what would
|
||||
// have been sent to Telegram / the webhook.
|
||||
pagedKeys := map[string]bool{}
|
||||
pagedPatterns := map[string]bool{}
|
||||
for _, sp := range s.pager.Filter(firing) {
|
||||
pagedKeys[spikeKey(sp)] = true
|
||||
for _, p := range sp.Patterns {
|
||||
pagedPatterns[patternKey(sp.Service, p.Signature)] = true
|
||||
}
|
||||
}
|
||||
|
||||
nowActive := map[string]bool{}
|
||||
@@ -198,7 +204,14 @@ func (s *scanner) Scan(ctx context.Context, now time.Time) snapshot {
|
||||
first = end
|
||||
s.active[key] = first
|
||||
}
|
||||
snap.Issues = append(snap.Issues, issueView{alert: alerts.NewAlert(sp, end), key: key, paged: pagedKeys[key], firstSeen: first})
|
||||
paged := false
|
||||
for _, p := range sp.Patterns {
|
||||
if pagedPatterns[patternKey(sp.Service, p.Signature)] {
|
||||
paged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
snap.Issues = append(snap.Issues, issueView{alert: alerts.NewAlert(sp, end), key: key, paged: paged, firstSeen: first})
|
||||
}
|
||||
for key, first := range s.active {
|
||||
if !nowActive[key] {
|
||||
|
||||
@@ -251,3 +251,26 @@ func TestBarAndMinimal(t *testing.T) {
|
||||
t.Fatalf("minimal render should surface scan errors, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelKeepsLastGoodSnapshotOnScanError(t *testing.T) {
|
||||
m := newModel(nil, modelConfig{Interval: time.Minute})
|
||||
good := snapshot{ScannedAt: time.Now(), Issues: []issueView{{}}}
|
||||
updated, _ := m.Update(snapshotMsg(good))
|
||||
m = updated.(model)
|
||||
|
||||
failed := snapshot{Err: context.DeadlineExceeded}
|
||||
updated, _ = m.Update(snapshotMsg(failed))
|
||||
m = updated.(model)
|
||||
if len(m.snap.Issues) != 1 {
|
||||
t.Fatalf("failed scan should not erase the last good snapshot: %+v", m.snap)
|
||||
}
|
||||
if m.scanErr == nil {
|
||||
t.Fatal("failed scan should be visible separately from the last good snapshot")
|
||||
}
|
||||
|
||||
updated, _ = m.Update(snapshotMsg(snapshot{ScannedAt: time.Now()}))
|
||||
m = updated.(model)
|
||||
if m.scanErr != nil {
|
||||
t.Fatal("successful scan should clear the previous scan error")
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -25,6 +25,9 @@ type model struct {
|
||||
sc *scanner
|
||||
cfg modelConfig
|
||||
snap snapshot
|
||||
// scanErr is kept separately so a failed refresh does not erase the last
|
||||
// successful data from the display.
|
||||
scanErr error
|
||||
|
||||
scanning bool
|
||||
nextScanAt time.Time
|
||||
@@ -79,7 +82,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
|
||||
case snapshotMsg:
|
||||
m.snap = snapshot(msg)
|
||||
snap := snapshot(msg)
|
||||
if snap.Err != nil {
|
||||
m.scanErr = snap.Err
|
||||
} else {
|
||||
m.snap = snap
|
||||
m.scanErr = nil
|
||||
}
|
||||
m.scanning = false
|
||||
m.nextScanAt = time.Now().Add(m.cfg.Interval)
|
||||
return m, nil
|
||||
@@ -90,6 +99,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m model) View() string {
|
||||
return renderSnapshot(m.snap, snapshotOptions{
|
||||
Width: m.width, Height: m.height, Title: m.cfg.Title, BaseURL: m.cfg.BaseURL,
|
||||
Scanning: m.scanning, NextScanAt: m.nextScanAt, Now: time.Now(),
|
||||
Scanning: m.scanning, NextScanAt: m.nextScanAt, Now: time.Now(), ScanError: m.scanErr,
|
||||
})
|
||||
}
|
||||
@@ -34,6 +34,7 @@ type snapshotOptions struct {
|
||||
Scanning bool
|
||||
NextScanAt time.Time
|
||||
Now time.Time
|
||||
ScanError error
|
||||
}
|
||||
|
||||
// Colors mirror the otel-ui design-system tokens (AGENTS.md) so the terminal
|
||||
@@ -87,6 +88,9 @@ func renderBody(snap snapshot, o snapshotOptions, w, avail int) string {
|
||||
case snap.Err != nil:
|
||||
return statusPanel(w, min(avail, 5), colCritical, "SCAN FAILED", snap.Err.Error(),
|
||||
"values are from "+fmtTime(snap.ScannedAt)+"; the display keeps the last good scan")
|
||||
case o.ScanError != nil && len(snap.Issues) == 0:
|
||||
return statusPanel(w, min(avail, 5), colCritical, "SCAN FAILED", o.ScanError.Error(),
|
||||
"no successful scan yet; retrying automatically")
|
||||
case len(snap.Issues) == 0:
|
||||
sub := fmt.Sprintf("no issues firing · window %s–%s UTC · scanned %s",
|
||||
snap.WindowStart.Format("15:04"), snap.WindowEnd.Format("15:04"), commas(snap.Events))
|
||||
@@ -234,6 +238,9 @@ func renderHeader(snap snapshot, o snapshotOptions) string {
|
||||
title := style(colAccent, true).Render(strings.ToUpper(o.Title))
|
||||
|
||||
left := []string{title}
|
||||
if o.ScanError != nil {
|
||||
left = append(left, style(colCritical, true).Render("SCAN FAILED"))
|
||||
}
|
||||
if !snap.WindowEnd.IsZero() {
|
||||
left = append(left, style(colText3, false).Render(
|
||||
fmt.Sprintf("%s–%s UTC", snap.WindowStart.Format("15:04"), snap.WindowEnd.Format("15:04"))))
|
||||
|
||||
Reference in new issue
Block a user