fix: dispatch alert.acknowledged + alert.resolved on UI ack/resolve
Spotted during the live Playwright sweep: clicking Acknowledge or Resolve updated the alert row but never fanned out a notification. The handlers went straight to Store.Acknowledge/Resolve, bypassing the hub. Add Engine.Acknowledge and Engine.Resolve that wrap the store call and dispatch the matching event to every enabled channel. The UI handlers prefer the engine path when wired, and fall back to the direct store call so unit tests that construct a Server without an engine still work. Use context.WithoutCancel for the goroutine dispatch — the request context is cancelled the instant the handler returns 204, so the naive 'go e.hub.Dispatch(ctx, ...)' was racing the response and losing the channel-list query with 'context canceled'.
This commit is contained in:
@@ -68,6 +68,60 @@ func (e *Engine) raiseAndNotify(ctx context.Context, hostID, kind, severity, mes
|
||||
})
|
||||
}
|
||||
|
||||
// Acknowledge updates the alert row and fans out alert.acknowledged to
|
||||
// every enabled channel. Best-effort: store errors are logged but the
|
||||
// dispatch still fires only when the store update succeeds.
|
||||
func (e *Engine) Acknowledge(ctx context.Context, alertID, userID string, when time.Time) error {
|
||||
if err := e.store.Acknowledge(ctx, alertID, userID, when); err != nil {
|
||||
return err
|
||||
}
|
||||
a, lerr := e.store.GetAlert(ctx, alertID)
|
||||
if lerr != nil || a == nil {
|
||||
// Acknowledge already succeeded; dispatch is best-effort.
|
||||
return nil //nolint:nilerr
|
||||
}
|
||||
p := alertPayload(ctx, e.store, notification.EventAcknowledged, a)
|
||||
go e.hub.Dispatch(context.WithoutCancel(ctx), p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve marks the alert resolved and fans out alert.resolved.
|
||||
func (e *Engine) Resolve(ctx context.Context, alertID string, when time.Time) error {
|
||||
a, _ := e.store.GetAlert(ctx, alertID)
|
||||
if err := e.store.Resolve(ctx, alertID, when); err != nil {
|
||||
return err
|
||||
}
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
p := alertPayload(ctx, e.store, notification.EventResolved, a)
|
||||
go e.hub.Dispatch(context.WithoutCancel(ctx), p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// alertPayload builds a Payload from a stored Alert, looking up the host
|
||||
// name when HostID is set.
|
||||
func alertPayload(ctx context.Context, st *store.Store, ev notification.Event, a *store.Alert) notification.Payload {
|
||||
hostID, hostName := "", ""
|
||||
if a.HostID != nil {
|
||||
hostID = *a.HostID
|
||||
hostName = hostID
|
||||
if h, err := st.GetHost(ctx, hostID); err == nil && h != nil {
|
||||
hostName = h.Name
|
||||
}
|
||||
}
|
||||
return notification.Payload{
|
||||
Event: ev,
|
||||
AlertID: a.ID,
|
||||
Severity: a.Severity,
|
||||
Kind: a.Kind,
|
||||
HostID: hostID,
|
||||
HostName: hostName,
|
||||
Message: a.Message,
|
||||
RaisedAt: a.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// resolveAndNotify clears every open (or acknowledged) alert for
|
||||
// (host_id, kind) via store.AutoResolve, then fires alert.resolved
|
||||
// for each row that was actually open. Best-effort — errors are
|
||||
|
||||
Reference in New Issue
Block a user