531 lines
15 KiB
Go
531 lines
15 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/license"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
const aiIntelligenceUpgradeURL = "https://pulsemonitor.app/pro"
|
|
|
|
// HandleGetPatterns returns detected failure patterns (GET /api/ai/intelligence/patterns)
|
|
func (h *AISettingsHandler) HandleGetPatterns(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
patrol := h.aiService.GetPatrolService()
|
|
if patrol == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"patterns": []interface{}{},
|
|
"message": "Patrol service not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write patterns response")
|
|
}
|
|
return
|
|
}
|
|
|
|
detector := patrol.GetPatternDetector()
|
|
if detector == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"patterns": []interface{}{},
|
|
"message": "Pattern detector not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write patterns response")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get resource filter if provided
|
|
resourceID := r.URL.Query().Get("resource_id")
|
|
|
|
patterns := detector.GetPatterns()
|
|
var result []map[string]interface{}
|
|
|
|
for key, pattern := range patterns {
|
|
if resourceID != "" && pattern.ResourceID != resourceID {
|
|
continue
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"key": key,
|
|
"resource_id": pattern.ResourceID,
|
|
"event_type": pattern.EventType,
|
|
"occurrences": pattern.Occurrences,
|
|
"average_interval": pattern.AverageInterval.String(),
|
|
"average_duration": pattern.AverageDuration.String(),
|
|
"last_occurrence": pattern.LastOccurrence,
|
|
"confidence": pattern.Confidence,
|
|
})
|
|
}
|
|
|
|
locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol)
|
|
if locked {
|
|
w.Header().Set("X-License-Required", "true")
|
|
w.Header().Set("X-License-Feature", license.FeatureAIPatrol)
|
|
}
|
|
|
|
count := len(result)
|
|
if locked {
|
|
result = []map[string]interface{}{}
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"patterns": result,
|
|
"count": count,
|
|
"license_required": locked,
|
|
"upgrade_url": aiIntelligenceUpgradeURL,
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write patterns response")
|
|
}
|
|
}
|
|
|
|
// HandleGetPredictions returns failure predictions (GET /api/ai/intelligence/predictions)
|
|
func (h *AISettingsHandler) HandleGetPredictions(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
patrol := h.aiService.GetPatrolService()
|
|
if patrol == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"predictions": []interface{}{},
|
|
"message": "Patrol service not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write predictions response")
|
|
}
|
|
return
|
|
}
|
|
|
|
detector := patrol.GetPatternDetector()
|
|
if detector == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"predictions": []interface{}{},
|
|
"message": "Pattern detector not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write predictions response")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get resource filter if provided
|
|
resourceID := r.URL.Query().Get("resource_id")
|
|
|
|
var predictions []ai.FailurePrediction
|
|
if resourceID != "" {
|
|
predictions = detector.GetPredictionsForResource(resourceID)
|
|
} else {
|
|
predictions = detector.GetPredictions()
|
|
}
|
|
|
|
var result []map[string]interface{}
|
|
for _, pred := range predictions {
|
|
isOverdue := pred.DaysUntil < 0
|
|
result = append(result, map[string]interface{}{
|
|
"resource_id": pred.ResourceID,
|
|
"event_type": pred.EventType,
|
|
"predicted_at": pred.PredictedAt,
|
|
"days_until": pred.DaysUntil,
|
|
"confidence": pred.Confidence,
|
|
"basis": pred.Basis,
|
|
"is_overdue": isOverdue,
|
|
})
|
|
}
|
|
|
|
locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol)
|
|
if locked {
|
|
w.Header().Set("X-License-Required", "true")
|
|
w.Header().Set("X-License-Feature", license.FeatureAIPatrol)
|
|
}
|
|
|
|
count := len(result)
|
|
if locked {
|
|
result = []map[string]interface{}{}
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"predictions": result,
|
|
"count": count,
|
|
"license_required": locked,
|
|
"upgrade_url": aiIntelligenceUpgradeURL,
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write predictions response")
|
|
}
|
|
}
|
|
|
|
// HandleGetCorrelations returns detected resource correlations (GET /api/ai/intelligence/correlations)
|
|
func (h *AISettingsHandler) HandleGetCorrelations(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
patrol := h.aiService.GetPatrolService()
|
|
if patrol == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"correlations": []interface{}{},
|
|
"message": "Patrol service not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write correlations response")
|
|
}
|
|
return
|
|
}
|
|
|
|
detector := patrol.GetCorrelationDetector()
|
|
if detector == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"correlations": []interface{}{},
|
|
"message": "Correlation detector not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write correlations response")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get resource filter if provided
|
|
resourceID := r.URL.Query().Get("resource_id")
|
|
|
|
var correlations []*ai.Correlation
|
|
if resourceID != "" {
|
|
correlations = detector.GetCorrelationsForResource(resourceID)
|
|
} else {
|
|
correlations = detector.GetCorrelations()
|
|
}
|
|
|
|
var result []map[string]interface{}
|
|
for _, corr := range correlations {
|
|
result = append(result, map[string]interface{}{
|
|
"source_id": corr.SourceID,
|
|
"source_name": corr.SourceName,
|
|
"source_type": corr.SourceType,
|
|
"target_id": corr.TargetID,
|
|
"target_name": corr.TargetName,
|
|
"target_type": corr.TargetType,
|
|
"event_pattern": corr.EventPattern,
|
|
"occurrences": corr.Occurrences,
|
|
"avg_delay": corr.AvgDelay.String(),
|
|
"confidence": corr.Confidence,
|
|
"last_seen": corr.LastSeen,
|
|
"description": corr.Description,
|
|
})
|
|
}
|
|
|
|
locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol)
|
|
if locked {
|
|
w.Header().Set("X-License-Required", "true")
|
|
w.Header().Set("X-License-Feature", license.FeatureAIPatrol)
|
|
}
|
|
|
|
count := len(result)
|
|
if locked {
|
|
result = []map[string]interface{}{}
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"correlations": result,
|
|
"count": count,
|
|
"license_required": locked,
|
|
"upgrade_url": aiIntelligenceUpgradeURL,
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write correlations response")
|
|
}
|
|
}
|
|
|
|
// HandleGetRecentChanges returns recent infrastructure changes (GET /api/ai/intelligence/changes)
|
|
func (h *AISettingsHandler) HandleGetRecentChanges(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
patrol := h.aiService.GetPatrolService()
|
|
if patrol == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"changes": []interface{}{},
|
|
"message": "Patrol service not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write changes response")
|
|
}
|
|
return
|
|
}
|
|
|
|
detector := patrol.GetChangeDetector()
|
|
if detector == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"changes": []interface{}{},
|
|
"message": "Change detector not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write changes response")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get time range - default to 24 hours
|
|
hoursStr := r.URL.Query().Get("hours")
|
|
hours := 24
|
|
if hoursStr != "" {
|
|
if h, err := strconv.Atoi(hoursStr); err == nil && h > 0 {
|
|
hours = h
|
|
}
|
|
}
|
|
|
|
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
|
changes := detector.GetRecentChanges(100, since)
|
|
|
|
var result []map[string]interface{}
|
|
for _, change := range changes {
|
|
result = append(result, map[string]interface{}{
|
|
"id": change.ID,
|
|
"resource_id": change.ResourceID,
|
|
"resource_name": change.ResourceName,
|
|
"resource_type": change.ResourceType,
|
|
"change_type": change.ChangeType,
|
|
"before": change.Before,
|
|
"after": change.After,
|
|
"detected_at": change.DetectedAt,
|
|
"description": change.Description,
|
|
})
|
|
}
|
|
|
|
locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol)
|
|
if locked {
|
|
w.Header().Set("X-License-Required", "true")
|
|
w.Header().Set("X-License-Feature", license.FeatureAIPatrol)
|
|
}
|
|
|
|
count := len(result)
|
|
if locked {
|
|
result = []map[string]interface{}{}
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"changes": result,
|
|
"count": count,
|
|
"hours": hours,
|
|
"license_required": locked,
|
|
"upgrade_url": aiIntelligenceUpgradeURL,
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write changes response")
|
|
}
|
|
}
|
|
|
|
// HandleGetBaselines returns learned resource baselines (GET /api/ai/intelligence/baselines)
|
|
func (h *AISettingsHandler) HandleGetBaselines(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
patrol := h.aiService.GetPatrolService()
|
|
if patrol == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"baselines": []interface{}{},
|
|
"message": "Patrol service not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write baselines response")
|
|
}
|
|
return
|
|
}
|
|
|
|
store := patrol.GetBaselineStore()
|
|
if store == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"baselines": []interface{}{},
|
|
"message": "Baseline store not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write baselines response")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get resource filter if provided
|
|
resourceID := r.URL.Query().Get("resource_id")
|
|
|
|
baselines := store.GetAllBaselines()
|
|
var result []map[string]interface{}
|
|
|
|
for key, baseline := range baselines {
|
|
if resourceID != "" && baseline.ResourceID != resourceID {
|
|
continue
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"key": key,
|
|
"resource_id": baseline.ResourceID,
|
|
"metric": baseline.Metric,
|
|
"mean": baseline.Mean,
|
|
"std_dev": baseline.StdDev,
|
|
"min": baseline.Min,
|
|
"max": baseline.Max,
|
|
"samples": baseline.Samples,
|
|
"last_update": baseline.LastUpdate,
|
|
})
|
|
}
|
|
|
|
locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol)
|
|
if locked {
|
|
w.Header().Set("X-License-Required", "true")
|
|
w.Header().Set("X-License-Feature", license.FeatureAIPatrol)
|
|
}
|
|
|
|
count := len(result)
|
|
if locked {
|
|
result = []map[string]interface{}{}
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"baselines": result,
|
|
"count": count,
|
|
"license_required": locked,
|
|
"upgrade_url": aiIntelligenceUpgradeURL,
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write baselines response")
|
|
}
|
|
}
|
|
|
|
// HandleGetRemediations returns remediation history (GET /api/ai/intelligence/remediations)
|
|
func (h *AISettingsHandler) HandleGetRemediations(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
patrol := h.aiService.GetPatrolService()
|
|
if patrol == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"remediations": []interface{}{},
|
|
"message": "Patrol service not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write remediations response")
|
|
}
|
|
return
|
|
}
|
|
|
|
remediationLog := patrol.GetRemediationLog()
|
|
if remediationLog == nil {
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"remediations": []interface{}{},
|
|
"message": "Remediation log not initialized",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write remediations response")
|
|
}
|
|
return
|
|
}
|
|
|
|
resourceID := r.URL.Query().Get("resource_id")
|
|
findingID := r.URL.Query().Get("finding_id")
|
|
|
|
limit := 20
|
|
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
|
if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
|
|
hours := 168
|
|
if hoursStr := r.URL.Query().Get("hours"); hoursStr != "" {
|
|
if parsed, err := strconv.Atoi(hoursStr); err == nil && parsed > 0 {
|
|
hours = parsed
|
|
}
|
|
}
|
|
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
|
|
|
var records []ai.RemediationRecord
|
|
switch {
|
|
case findingID != "":
|
|
records = remediationLog.GetForFinding(findingID, limit)
|
|
case resourceID != "":
|
|
records = remediationLog.GetForResource(resourceID, limit)
|
|
default:
|
|
records = remediationLog.GetRecentRemediations(limit, since)
|
|
}
|
|
|
|
stats := remediationStatsFromRecords(records)
|
|
if findingID == "" && resourceID == "" {
|
|
stats = remediationLog.GetRecentRemediationStats(since)
|
|
}
|
|
|
|
result := make([]map[string]interface{}, 0, len(records))
|
|
for _, rec := range records {
|
|
durationMs := int64(0)
|
|
if rec.Duration > 0 {
|
|
durationMs = rec.Duration.Milliseconds()
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"id": rec.ID,
|
|
"timestamp": rec.Timestamp,
|
|
"resource_id": rec.ResourceID,
|
|
"resource_type": rec.ResourceType,
|
|
"resource_name": rec.ResourceName,
|
|
"finding_id": rec.FindingID,
|
|
"problem": rec.Problem,
|
|
"action": rec.Action,
|
|
"output": rec.Output,
|
|
"outcome": rec.Outcome,
|
|
"duration_ms": durationMs,
|
|
"note": rec.Note,
|
|
"automatic": rec.Automatic,
|
|
})
|
|
}
|
|
|
|
locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol)
|
|
if locked {
|
|
w.Header().Set("X-License-Required", "true")
|
|
w.Header().Set("X-License-Feature", license.FeatureAIPatrol)
|
|
}
|
|
|
|
count := len(result)
|
|
if locked {
|
|
result = []map[string]interface{}{}
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
|
"remediations": result,
|
|
"count": count,
|
|
"stats": stats,
|
|
"license_required": locked,
|
|
"upgrade_url": aiIntelligenceUpgradeURL,
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write remediations response")
|
|
}
|
|
}
|
|
|
|
func remediationStatsFromRecords(records []ai.RemediationRecord) map[string]int {
|
|
stats := map[string]int{
|
|
"total": len(records),
|
|
"resolved": 0,
|
|
"partial": 0,
|
|
"failed": 0,
|
|
"unknown": 0,
|
|
"automatic": 0,
|
|
"manual": 0,
|
|
}
|
|
|
|
for _, rec := range records {
|
|
switch rec.Outcome {
|
|
case ai.OutcomeResolved:
|
|
stats["resolved"]++
|
|
case ai.OutcomePartial:
|
|
stats["partial"]++
|
|
case ai.OutcomeFailed:
|
|
stats["failed"]++
|
|
default:
|
|
stats["unknown"]++
|
|
}
|
|
if rec.Automatic {
|
|
stats["automatic"]++
|
|
} else {
|
|
stats["manual"]++
|
|
}
|
|
}
|
|
|
|
return stats
|
|
}
|