mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
3c8705da92
v6.1.0-rc.4 shipped the attention workbench gated on monitoring:read only, so mobile relay tokens (which carry just relay:mobile-access) got 403 on alert sync, finding detail, and acknowledge the moment a server upgraded to rc.4. The attention routes supersede the legacy patrol findings routes, which accepted the mobile capability, so they join the governed relay mobile runtime route inventory: reads accept relay:mobile-access alongside monitoring:read (legacy ai:execute), mutations alongside monitoring:write.
152 lines
3.7 KiB
Go
152 lines
3.7 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
|
)
|
|
|
|
const (
|
|
maxAttentionSuppressionDuration = 30 * 24 * time.Hour
|
|
maxAttentionMutationBodyBytes = 4096
|
|
)
|
|
|
|
type attentionMutationKind string
|
|
|
|
const (
|
|
attentionMutationAcknowledge attentionMutationKind = "acknowledge"
|
|
attentionMutationUnacknowledge attentionMutationKind = "unacknowledge"
|
|
attentionMutationSuppress attentionMutationKind = "suppress"
|
|
attentionMutationUnsuppress attentionMutationKind = "unsuppress"
|
|
)
|
|
|
|
type attentionSuppressionRequest struct {
|
|
Reason string `json:"reason"`
|
|
ExpiresAt time.Time `json:"expiresAt"`
|
|
}
|
|
|
|
func parseAttentionMutationPath(
|
|
path string,
|
|
) (string, attentionMutationKind, bool) {
|
|
path = strings.Trim(path, "/")
|
|
for _, kind := range []attentionMutationKind{
|
|
attentionMutationAcknowledge,
|
|
attentionMutationUnacknowledge,
|
|
attentionMutationSuppress,
|
|
attentionMutationUnsuppress,
|
|
} {
|
|
suffix := "/" + string(kind)
|
|
if !strings.HasSuffix(path, suffix) {
|
|
continue
|
|
}
|
|
itemID, err := url.PathUnescape(strings.TrimSuffix(path, suffix))
|
|
itemID = strings.TrimSpace(itemID)
|
|
return itemID, kind, err == nil && itemID != ""
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
func isAttentionMutationPath(path string) bool {
|
|
_, _, ok := parseAttentionMutationPath(path)
|
|
return ok
|
|
}
|
|
|
|
func (h *AttentionHandlers) handleAttentionMutation(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
path string,
|
|
) {
|
|
if !ensureRelayMobileRuntimeRoute(w, r, relayMobileRouteAttentionMutation) {
|
|
return
|
|
}
|
|
itemID, kind, ok := parseAttentionMutationPath(path)
|
|
if !ok {
|
|
writeJSONError(
|
|
w,
|
|
http.StatusNotFound,
|
|
"attention_mutation_not_found",
|
|
"Attention mutation was not found.",
|
|
)
|
|
return
|
|
}
|
|
if h == nil || h.getMonitor == nil {
|
|
writeAttentionUnavailable(w, nil)
|
|
return
|
|
}
|
|
monitor := h.getMonitor(r.Context())
|
|
if monitor == nil || monitor.GetAlertManager() == nil {
|
|
writeAttentionUnavailable(w, nil)
|
|
return
|
|
}
|
|
manager := monitor.GetAlertManager()
|
|
actor := strings.TrimSpace(auth.GetUser(r.Context()))
|
|
if actor == "" {
|
|
actor = "unknown"
|
|
}
|
|
|
|
var err error
|
|
switch kind {
|
|
case attentionMutationAcknowledge:
|
|
err = manager.AcknowledgeAlert(itemID, actor)
|
|
case attentionMutationUnacknowledge:
|
|
err = manager.UnacknowledgeAlert(itemID)
|
|
case attentionMutationSuppress:
|
|
var request attentionSuppressionRequest
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxAttentionMutationBodyBytes)
|
|
if decodeErr := json.NewDecoder(r.Body).Decode(&request); decodeErr != nil {
|
|
writeJSONError(
|
|
w,
|
|
http.StatusBadRequest,
|
|
"invalid_attention_suppression",
|
|
"Suppression requires a reason and expiry.",
|
|
)
|
|
return
|
|
}
|
|
now := time.Now().UTC()
|
|
if strings.TrimSpace(request.Reason) == "" ||
|
|
request.ExpiresAt.IsZero() ||
|
|
!request.ExpiresAt.After(now) ||
|
|
request.ExpiresAt.After(now.Add(maxAttentionSuppressionDuration)) {
|
|
writeJSONError(
|
|
w,
|
|
http.StatusBadRequest,
|
|
"invalid_attention_suppression",
|
|
"Suppression requires a reason and an expiry within 30 days.",
|
|
)
|
|
return
|
|
}
|
|
expiresAt := request.ExpiresAt.UTC()
|
|
err = manager.SuppressOperationalAlert(
|
|
itemID,
|
|
actor,
|
|
request.Reason,
|
|
&expiresAt,
|
|
)
|
|
case attentionMutationUnsuppress:
|
|
err = manager.UnsuppressOperationalAlert(itemID)
|
|
}
|
|
if err != nil {
|
|
writeJSONError(
|
|
w,
|
|
http.StatusNotFound,
|
|
"attention_item_not_found",
|
|
"Attention item was not found.",
|
|
)
|
|
return
|
|
}
|
|
monitor.SyncAlertState()
|
|
if err := utils.WriteJSONResponse(w, map[string]bool{"success": true}); err != nil {
|
|
writeJSONError(
|
|
w,
|
|
http.StatusInternalServerError,
|
|
"attention_mutation_encode_failed",
|
|
"Failed to encode attention mutation result.",
|
|
)
|
|
}
|
|
}
|