package server import ( "encoding/json" "fmt" "log" "net/http" "strings" "github.com/gin-gonic/gin" "warpbox/lib/metastore" ) type AlertPageView struct { PageTitle string WindowTitle string WindowIcon string PageScripts []string AccountNav AccountNavView CSRFToken string Filters AlertFiltersView Stats AlertStatsView Alerts []AlertRowView SelectedAlert *AlertRowView Groups []string CanManageAlerts bool } type AlertFiltersView struct { Query string Severity string Status string Group string Sort string } type AlertStatsView struct { Open int Acknowledged int Closed int High int Medium int Low int } type AlertRowView struct { ID string Title string Description string Severity string Status string Code string Trace string Group string MetadataPretty string CreatedAt string UpdatedAt string } func (app *App) handleAccountAlerts(ctx *gin.Context) { actor, ok := currentAccountUser(ctx) if !ok { ctx.Redirect(http.StatusSeeOther, "/account/login") return } page, err := app.ListAlerts(ctx, actor, accountAlertFiltersFromRequest(ctx)) if err != nil { ctx.String(http.StatusForbidden, "Permission denied") return } ctx.HTML(http.StatusOK, "account_alerts.html", page) } func (app *App) handleAccountAlertAcknowledge(ctx *gin.Context) { app.handleAccountAlertAction(ctx, func(actor metastore.User, id string) error { return app.AcknowledgeAlert(ctx, actor, id) }) } func (app *App) handleAccountAlertClose(ctx *gin.Context) { app.handleAccountAlertAction(ctx, func(actor metastore.User, id string) error { return app.CloseAlert(ctx, actor, id) }) } func (app *App) handleAccountAlertBulkAcknowledge(ctx *gin.Context) { app.handleAccountAlertBulkAction(ctx, func(actor metastore.User, ids []string) error { return app.BulkAcknowledgeAlerts(ctx, actor, ids) }) } func (app *App) handleAccountAlertBulkClose(ctx *gin.Context) { app.handleAccountAlertBulkAction(ctx, func(actor metastore.User, ids []string) error { return app.BulkCloseAlerts(ctx, actor, ids) }) } func (app *App) handleAccountAlertsExport(ctx *gin.Context) { actor, ok := currentAccountUser(ctx) if !ok { ctx.Redirect(http.StatusSeeOther, "/account/login") return } page, err := app.ListAlerts(ctx, actor, accountAlertFiltersFromRequest(ctx)) if err != nil { ctx.String(http.StatusForbidden, "Permission denied") return } ctx.Header("Content-Disposition", `attachment; filename="warpbox-alerts.json"`) ctx.JSON(http.StatusOK, gin.H{"alerts": page.Alerts, "filters": page.Filters, "stats": page.Stats}) } func (app *App) handleAccountAlertAction(ctx *gin.Context, action func(metastore.User, string) error) { actor, ok := currentAccountUser(ctx) if !ok { ctx.Redirect(http.StatusSeeOther, "/account/login") return } if err := action(actor, ctx.Param("id")); err != nil { ctx.String(http.StatusForbidden, err.Error()) return } ctx.Redirect(http.StatusSeeOther, "/account/alerts") } func (app *App) handleAccountAlertBulkAction(ctx *gin.Context, action func(metastore.User, []string) error) { actor, ok := currentAccountUser(ctx) if !ok { ctx.Redirect(http.StatusSeeOther, "/account/login") return } if err := action(actor, ctx.PostFormArray("alert_ids")); err != nil { ctx.String(http.StatusForbidden, err.Error()) return } ctx.Redirect(http.StatusSeeOther, "/account/alerts") } func (app *App) CreateAlert(ctx *gin.Context, actor metastore.User, input metastore.AlertInput) (metastore.Alert, error) { if err := app.requireAlertManage(ctx); err != nil { return metastore.Alert{}, err } if input.CreatedBy == "" { input.CreatedBy = actor.Username } return app.store.CreateAlert(input) } func (app *App) ListAlerts(ctx *gin.Context, actor metastore.User, filters metastore.AlertFilters) (AlertPageView, error) { if err := app.requireAlertView(ctx); err != nil { return AlertPageView{}, err } alerts, err := app.store.ListAlerts(filters) if err != nil { return AlertPageView{}, err } rows := make([]AlertRowView, 0, len(alerts)) stats := AlertStatsView{} groupSet := map[string]bool{} for _, alert := range alerts { row := alertRowView(alert) rows = append(rows, row) groupSet[row.Group] = true switch alert.Status { case metastore.AlertStatusAcknowledged: stats.Acknowledged++ case metastore.AlertStatusClosed: stats.Closed++ default: stats.Open++ } switch alert.Severity { case metastore.AlertSeverityHigh: stats.High++ case metastore.AlertSeverityMedium: stats.Medium++ default: stats.Low++ } } groups := make([]string, 0, len(groupSet)) for group := range groupSet { groups = append(groups, group) } if len(groups) == 0 { groups = []string{"system"} } nav := app.accountNavView(ctx, "alerts") nav.AlertCount, nav.AlertSeverity = app.openAlertSummary() var selected *AlertRowView if len(rows) > 0 { selected = &rows[0] } return AlertPageView{ PageTitle: "WarpBox Alerts", WindowTitle: "WarpBox Alerts", WindowIcon: "!", PageScripts: []string{"/static/js/account-alerts.js"}, AccountNav: nav, CSRFToken: app.currentCSRFToken(ctx), Filters: AlertFiltersView{Query: filters.Query, Severity: filters.Severity, Status: filters.Status, Group: filters.Group, Sort: filters.Sort}, Stats: stats, Alerts: rows, SelectedAlert: selected, Groups: groups, CanManageAlerts: currentAccountPermissions(ctx).AdminAccess, }, nil } func (app *App) AcknowledgeAlert(ctx *gin.Context, actor metastore.User, id string) error { if err := app.requireAlertManage(ctx); err != nil { return err } return app.store.AcknowledgeAlert(id) } func (app *App) CloseAlert(ctx *gin.Context, actor metastore.User, id string) error { if err := app.requireAlertManage(ctx); err != nil { return err } return app.store.CloseAlert(id) } func (app *App) BulkAcknowledgeAlerts(ctx *gin.Context, actor metastore.User, ids []string) error { if err := app.requireAlertManage(ctx); err != nil { return err } for _, id := range uniqueNonEmpty(ids) { if err := app.store.AcknowledgeAlert(id); err != nil { return err } } return nil } func (app *App) BulkCloseAlerts(ctx *gin.Context, actor metastore.User, ids []string) error { if err := app.requireAlertManage(ctx); err != nil { return err } for _, id := range uniqueNonEmpty(ids) { if err := app.store.CloseAlert(id); err != nil { return err } } return nil } func (app *App) EmitSystemAlert(code string, severity string, title string, description string, trace string, metadata any) error { raw, err := json.Marshal(metadata) if err != nil { log.Printf("alert metadata marshal failed: %v", err) return err } _, err = app.store.CreateAlert(metastore.AlertInput{ Title: title, Description: description, Severity: severity, Code: code, Trace: trace, Metadata: raw, CreatedBy: "system", }) if err != nil { log.Printf("alert persistence failed: %v", err) } return err } func (app *App) requireAlertView(ctx *gin.Context) error { if !currentAccountPermissions(ctx).AdminAccess { return fmt.Errorf("permission denied") } return nil } func (app *App) requireAlertManage(ctx *gin.Context) error { if !currentAccountPermissions(ctx).AdminAccess { return fmt.Errorf("permission denied") } return nil } func accountAlertFiltersFromRequest(ctx *gin.Context) metastore.AlertFilters { return metastore.AlertFilters{ Query: strings.TrimSpace(ctx.Query("q")), Severity: emptyAsAll(ctx.Query("severity")), Status: emptyAsAll(ctx.Query("status")), Group: emptyAsAll(ctx.Query("group")), Sort: emptyAsNewest(ctx.Query("sort")), } } func emptyAsAll(value string) string { value = strings.TrimSpace(value) if value == "" { return "all" } return value } func emptyAsNewest(value string) string { value = strings.TrimSpace(value) if value == "" { return "newest" } return value } func alertRowView(alert metastore.Alert) AlertRowView { return AlertRowView{ ID: alert.ID, Title: alert.Title, Description: alert.Description, Severity: alert.Severity, Status: alert.Status, Code: alert.Code, Trace: alert.Trace, Group: alertGroupFromTrace(alert.Trace), MetadataPretty: prettyAlertMetadata(alert.Metadata), CreatedAt: formatAdminTime(alert.CreatedAt), UpdatedAt: formatAdminTime(alert.UpdatedAt), } } func prettyAlertMetadata(raw json.RawMessage) string { if len(raw) == 0 { return "{}" } var value any if err := json.Unmarshal(raw, &value); err != nil { return string(raw) } pretty, err := json.MarshalIndent(value, "", " ") if err != nil { return string(raw) } return string(pretty) } func alertGroupFromTrace(trace string) string { trace = strings.TrimSpace(trace) if trace == "" { return "system" } before, _, found := strings.Cut(trace, ".") if !found || before == "" { return "system" } return strings.ToLower(before) } func (app *App) openAlertSummary() (int, string) { alerts, err := app.store.ListAlerts(metastore.AlertFilters{Status: metastore.AlertStatusOpen}) if err != nil { return 0, "ok" } severity := "ok" for _, alert := range alerts { if alert.Severity == metastore.AlertSeverityHigh { return len(alerts), "danger" } if alert.Severity == metastore.AlertSeverityMedium { severity = "warning" } else if severity == "ok" { severity = "info" } } return len(alerts), severity } func uniqueNonEmpty(values []string) []string { seen := map[string]bool{} out := []string{} for _, value := range values { value = strings.TrimSpace(value) if value == "" || seen[value] { continue } seen[value] = true out = append(out, value) } return out }