server/internal/handlers/incidents.go
Rama main · réplica de lectura del repositorio original.
package handlers
import (
"encoding/json"
"errors"
"log"
"net/http"
"strconv"
"strings"
"time"
"blackbox/server/internal/models"
"blackbox/shared/types"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
)
func ListIncidents(database *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
status := q.Get("status")
confidence := q.Get("confidence")
service := q.Get("service")
limitStr := q.Get("limit")
limit := 50
if n, err := strconv.Atoi(limitStr); err == nil && n > 0 && n <= 200 {
limit = n
}
tx := database.Model(&models.Incident{}).Order("opened_at DESC").Limit(limit + 1)
if status != "" {
tx = tx.Where("status = ?", status)
}
if confidence != "" {
tx = tx.Where("confidence = ?", confidence)
}
if service != "" {
escapedService := strings.NewReplacer(
"\\", "\\\\",
"%", "\\%",
"_", "\\_",
).Replace(service)
tx = tx.Where(`services LIKE ? ESCAPE '\'`, "%\""+escapedService+"\"%")
}
var incidents []models.Incident
if err := tx.Find(&incidents).Error; err != nil {
writeError(w, http.StatusInternalServerError, "failed to fetch incidents")
return
}
type response struct {
Incidents []models.Incident `json:"incidents"`
HasMore bool `json:"has_more"`
}
resp := response{Incidents: incidents}
if len(incidents) > limit {
resp.Incidents = incidents[:limit]
resp.HasMore = true
}
if resp.Incidents == nil {
resp.Incidents = []models.Incident{}
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("ListIncidents encode: %v", err)
}
}
}
func GetIncidentSummary(database *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var summary struct {
OpenCount int64 `gorm:"column:open_count"`
ConfirmedOpenCount int64 `gorm:"column:confirmed_open_count"`
}
if err := database.Model(&models.Incident{}).
Where("status = ?", "open").
Select(`
COALESCE(COUNT(*), 0) AS open_count,
COALESCE(SUM(CASE WHEN confidence = 'confirmed' THEN 1 ELSE 0 END), 0) AS confirmed_open_count
`).
Scan(&summary).Error; err != nil {
writeError(w, http.StatusInternalServerError, "failed to fetch incident summary")
return
}
resp := struct {
OpenCount int64 `json:"open_count"`
HasConfirmedOpen bool `json:"has_confirmed_open"`
}{
OpenCount: summary.OpenCount,
HasConfirmedOpen: summary.ConfirmedOpenCount > 0,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("GetIncidentSummary encode: %v", err)
}
}
}
type incidentEntryDetail struct {
Link models.IncidentEntry `json:"link"`
Data *types.Entry `json:"entry"`
}
type incidentMembership struct {
ID string `json:"id"`
Confidence string `json:"confidence"`
}
type incidentMembershipRow struct {
EntryID string `gorm:"column:entry_id"`
IncidentID string `gorm:"column:incident_id"`
Confidence string `gorm:"column:confidence"`
OpenedAt time.Time `gorm:"column:opened_at"`
}
func ListIncidentMembership(database *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
EntryIDs []string `json:"entry_ids"`
}
if !decodeJSONBody(w, r, 32<<10, &req) {
return
}
if len(req.EntryIDs) > 200 {
writeError(w, http.StatusBadRequest, "entry_ids must contain at most 200 IDs")
return
}
entryIDs := make([]string, 0, len(req.EntryIDs))
seen := make(map[string]struct{}, len(req.EntryIDs))
for _, entryID := range req.EntryIDs {
entryID = strings.TrimSpace(entryID)
if entryID == "" {
continue
}
if _, ok := seen[entryID]; ok {
continue
}
seen[entryID] = struct{}{}
entryIDs = append(entryIDs, entryID)
}
resp := struct {
Memberships map[string]incidentMembership `json:"memberships"`
}{
Memberships: map[string]incidentMembership{},
}
if len(entryIDs) == 0 {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("ListIncidentMembership encode empty: %v", err)
}
return
}
var rows []incidentMembershipRow
if err := database.Table("incident_entries AS ie").
Select("ie.entry_id, incidents.id AS incident_id, incidents.confidence, incidents.opened_at").
Joins("JOIN incidents ON incidents.id = ie.incident_id").
Where("ie.entry_id IN ? AND incidents.status = ?", entryIDs, "open").
Order("incidents.opened_at DESC").
Scan(&rows).Error; err != nil {
writeError(w, http.StatusInternalServerError, "failed to fetch incident membership")
return
}
for _, row := range rows {
if _, ok := resp.Memberships[row.EntryID]; ok {
continue
}
resp.Memberships[row.EntryID] = incidentMembership{
ID: row.IncidentID,
Confidence: row.Confidence,
}
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("ListIncidentMembership encode: %v", err)
}
}
}
func GetIncident(database *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var incident models.Incident
if err := database.First(&incident, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
writeError(w, http.StatusNotFound, "incident not found")
return
}
writeError(w, http.StatusInternalServerError, "failed to fetch incident")
return
}
var links []models.IncidentEntry
if err := database.Where("incident_id = ?", id).
Order("score DESC").Find(&links).Error; err != nil {
writeError(w, http.StatusInternalServerError, "failed to fetch incident entries")
return
}
details := make([]incidentEntryDetail, 0, len(links))
for _, link := range links {
var entry types.Entry
if err := database.First(&entry, "id = ?", link.EntryID).Error; err != nil {
log.Printf("GetIncident missing entry for incident %s entry %s role %s: %v", link.IncidentID, link.EntryID, link.Role, err)
continue
}
details = append(details, incidentEntryDetail{Link: link, Data: &entry})
}
type response struct {
Incident models.Incident `json:"incident"`
Entries []incidentEntryDetail `json:"entries"`
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response{Incident: incident, Entries: details}); err != nil {
log.Printf("GetIncident encode: %v", err)
}
}
}