Pulse/pkg/reporting/reporting.go
rcourtman d4463a615c Add fleet-level AI narrative for multi-resource reports
The single-resource AI narrative landed in b2bd9d114 but multi-resource
fleet reports stayed heuristic-only. That left a gap on the exact axis
where AI helps most: a 50-resource fleet PDF is where synthesis is the
difference between useful and unread.

Introduce FleetNarrator as a separate interface from Narrator. The
input shapes are different — single-resource takes one set of metric
stats with a prior window, fleet takes a denormalised cross-resource
view with per-resource summaries plus a fleet aggregate.
HeuristicFleetNarrator owns the deterministic fallback: ranks
resources by severity (critical alerts > unhealthy disks > storage
pressure > memory > CPU > non-critical alerts), picks up to 5
outliers, derives cross-cutting patterns by counting how many of N
resources share a hot signal, and emits fleet-scoped recommendations.

internal/ai.Service implements FleetNarrator through
report_fleet_narrator.go. Distinct use-case label
(report_narrative_fleet) so fleet vs single-resource spend is
separable in the cost ledger and budget gate. The fleet payload is
denormalised through buildReportFleetPayload so prompt cost scales
linearly with fleet size. Same fail-closed invariant — nil provider,
parse failure, or context cancellation falls through to the heuristic.

Single-resource Narrator is intentionally NOT propagated through
engine.GenerateMulti: a 50-resource fleet report performs one AI call
(fleet narrator), not 51. The router resolver returns the AI service
for all three roles (Narrator, FleetNarrator, FindingsProvider).

The fleet PDF renders the FleetNarrative in the fleet summary cover
when present: executive prose, named outliers with severity-coloured
bullets, cross-cutting patterns, recommendations, optional period
comparison, and an AI provenance footer. The deterministic resource
summary table is preserved above so every named outlier is verifiable
against the table immediately below it. Legacy "Highest CPU / Most
alerts" bullets remain as the fallback when no FleetNarrative is
attached.
2026-05-10 21:23:12 +01:00

172 lines
5.1 KiB
Go

package reporting
import (
"time"
)
// ReportFormat represents the output format of a report
type ReportFormat string
const (
FormatCSV ReportFormat = "csv"
FormatPDF ReportFormat = "pdf"
)
// MetricReportRequest defines the parameters for generating a report
type MetricReportRequest struct {
ResourceType string
ResourceID string
MetricType string // Optional, if empty all metrics for the resource are included
Start time.Time
End time.Time
Format ReportFormat
Title string
// Optional enrichment data (populated by handler from monitor state)
Resource *ResourceInfo // Details about the resource being reported on
Alerts []AlertInfo // Active and recently resolved alerts for this resource
Backups []BackupInfo // Backup information for VMs/containers
Storage []StorageInfo // Storage pools (for nodes)
Disks []DiskInfo // Physical disk health (for nodes)
// Optional narrative interpretation. When Narrator is non-nil the
// engine builds a NarrativeInput from the queried report data and asks
// it to produce the executive summary; on error or nil it falls back to
// the heuristic narrator. Findings are passed through to NarrativeInput
// so a narrator can reference Patrol activity in the period.
Narrator Narrator
FindingsProvider FindingsProvider
}
// ResourceInfo contains details about the resource being reported on
type ResourceInfo struct {
Name string
DisplayName string
Status string
Host string // URL for nodes
Node string // Parent node for VMs/containers
Instance string // Proxmox instance name
Uptime int64
KernelVersion string
PVEVersion string
OSName string
OSVersion string
IPAddresses []string
CPUModel string
CPUCores int
CPUSockets int
MemoryTotal int64
DiskTotal int64
LoadAverage []float64
Temperature *float64 // CPU temp if available
Tags []string
ClusterName string
IsCluster bool
}
// AlertInfo contains alert information for the report
type AlertInfo struct {
Type string
Level string // warning, critical
Message string
Value float64
Threshold float64
StartTime time.Time
ResolvedTime *time.Time // nil if still active
Acknowledged bool
}
// BackupInfo contains backup information for VMs/containers
type BackupInfo struct {
Type string // vzdump, pbs
Storage string
Timestamp time.Time
Size int64
Verified bool
Protected bool
VolID string
NextBackup *time.Time
}
// StorageInfo contains storage pool information
type StorageInfo struct {
Name string
Type string // lvm, zfs, dir, nfs, etc.
Status string
Total int64
Used int64
Available int64
UsagePerc float64
Content string // images, rootdir, backup, etc.
ZFSHealth string // For ZFS pools
ZFSErrors int // Checksum/read/write errors
}
// DiskInfo contains physical disk health information
type DiskInfo struct {
Device string
Model string
Serial string
Type string // nvme, ssd, hdd
Size int64
Health string // PASSED, FAILED, UNKNOWN
Temperature int // Celsius
WearLevel int // 0-100, percentage of life REMAINING (100 = healthy, 0 = end of life, -1 = unknown)
}
// MultiReportRequest defines the parameters for generating a multi-resource report.
type MultiReportRequest struct {
Resources []MetricReportRequest // One per resource, each with enrichment
Format ReportFormat
Start time.Time
End time.Time
Title string
MetricType string
// Optional fleet-level narrative interpretation. When FleetNarrator is
// non-nil the engine builds a FleetNarrativeInput from the queried
// per-resource report data and asks it to produce the cross-resource
// summary; on error or nil it falls back to the heuristic fleet
// narrator. FindingsProvider, when set, is consulted per-resource so
// patrol findings can flow into per-resource narratives.
FleetNarrator FleetNarrator
Narrator Narrator
FindingsProvider FindingsProvider
}
// MultiReportData holds the data for multi-resource report generation.
type MultiReportData struct {
Title string
Start time.Time
End time.Time
GeneratedAt time.Time
Resources []*ReportData // Reuse existing ReportData per resource
TotalPoints int
// Fleet-level narrative interpretation, populated by the engine when
// the request supplies a FleetNarrator (or always populated with the
// heuristic fallback). The renderer prefers this over recomputing
// observations inline.
FleetNarrative *FleetNarrative
}
// Engine defines the interface for report generation.
// This allows the enterprise version to provide PDF/CSV generation.
type Engine interface {
Generate(req MetricReportRequest) (data []byte, contentType string, err error)
GenerateMulti(req MultiReportRequest) (data []byte, contentType string, err error)
}
var (
globalEngine Engine
)
// SetEngine sets the global report engine.
func SetEngine(e Engine) {
globalEngine = e
}
// GetEngine returns the current global report engine.
func GetEngine() Engine {
return globalEngine
}