wire PDM alert bridge into PatrolService and seed demo

This commit is contained in:
rcourtman 2026-05-13 05:03:13 +01:00
parent b9844f22c1
commit 99d0edb04a
4 changed files with 40 additions and 2 deletions

View file

@ -159,6 +159,23 @@ func (p *PatrolService) InjectDemoFindings() {
TimesRaised: 2,
Source: updateSafetySource,
},
{
ID: "demo-pdm-alert-node-offline",
Key: PDMAlertFindingPrefix + ":datacenter-a/node/pve-edge-01",
Severity: FindingSeverityWarning,
Category: FindingCategoryReliability,
Title: "PDM: node \"pve-edge-01\" is offline",
Description: "Proxmox Datacenter Manager reports node \"pve-edge-01\" in remote \"datacenter-a\" is offline.",
ResourceID: "datacenter-a/node/pve-edge-01",
ResourceName: "pve-edge-01",
ResourceType: "node",
Node: "datacenter-a",
Evidence: "status=offline remote=datacenter-a",
DetectedAt: now.Add(-8 * time.Minute),
LastSeenAt: now.Add(-2 * time.Minute),
TimesRaised: 1,
Source: pdmAlertSourceLabel,
},
}
// Add findings to the store

View file

@ -32,8 +32,8 @@ func TestPatrolService_InjectDemoFindings(t *testing.T) {
service.InjectDemoFindings()
findings := service.findings.GetAll(nil)
if len(findings) != 6 {
t.Fatalf("expected 6 demo findings, got %d", len(findings))
if len(findings) != 7 {
t.Fatalf("expected 7 demo findings, got %d", len(findings))
}
if service.runHistoryStore.Count() != 13 {
t.Fatalf("expected 13 demo run history entries, got %d", service.runHistoryStore.Count())

View file

@ -464,6 +464,11 @@ type PatrolService struct {
// lifetime; in-memory only, no goroutine.
stormThrottler *findingStormThrottler
updateSafetyWatcher *UpdateSafetyWatcher
// PDM alert bridge -- polls the PDM resource list on each patrol cycle
// and emits reliability findings for offline nodes and failed guests.
// Nil source at MVP makes Observe a no-op; the real HTTP client is a
// follow-on.
pdmAlertBridge *pdmAlertBridge
// ReadState provides typed read-only views over resource state (VMs, nodes, hosts, etc.).
// This is injected separately from stateProvider since stateProvider also contains
// non-resource telemetry (alerts, backups, connection health) that isn't modeled as resources yet.
@ -687,5 +692,6 @@ func NewPatrolService(aiService *Service, stateProvider StateProvider) *PatrolSe
p.stormThrottler = newFindingStormThrottler()
p.findings.SetStormThrottler(p.stormThrottler)
p.updateSafetyWatcher = newUpdateSafetyWatcher()
p.pdmAlertBridge = newPDMAlertBridge(nil)
return p
}

View file

@ -423,6 +423,21 @@ func (p *PatrolService) runPatrolWithTrigger(ctx context.Context, trigger Trigge
}
}
// Run PDM alert bridge on every patrol cycle if a source is configured.
// Nil source guard mirrors the MVP no-op behavior; the real HTTP client
// is wired in a follow-on. Direct FindingsStore.Add bypasses push.
if p.pdmAlertBridge != nil && p.pdmAlertBridge.source != nil {
pdmEmit, pdmResolve := p.pdmAlertBridge.Observe(ctx, time.Now())
for _, f := range pdmEmit {
if !p.findings.Add(f) {
log.Debug().Str("finding_id", f.ID).Msg("AI Patrol: PDM alert finding already in store")
}
}
for _, s := range pdmResolve {
p.findings.ResolveWithReason(s.DedupKey, s.Reason)
}
}
// Determine if we can run LLM analysis (requires AI service + circuit breaker not open)
aiServiceEnabled := p.aiService != nil && p.aiService.IsEnabled()
canRunLLM := aiServiceEnabled && llmAllowed