Ctrl K
ComunidadGuías…

server/internal/handlers/incidents.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
		}
	}
}