mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
fix(reporting): resolve report subjects through the canonical unified view and paginate the metric card grid
Two regressions surfaced by generating a real-mode agent report after a backend restart: - Report subject lookups used Monitor.GetUnifiedResources (the raw resource store), but the raw store's canonical IDs depend on per-boot ingest order for merged-source hosts: after a restart the same host resolved to a different agent-<hash> than the one the UI and /api/state advertise, so reports lost the resource name, availability, and metrics translation entirely. Subject enrichment now reads Monitor.UnifiedResourceSnapshot and MetricsTargetForResource resolves through GetUnifiedReadStateOrSnapshot first (raw store as fallback) - the same re-ingested registry every other read surface uses. - The performance summary card grid positions cards absolutely and never paginated: an agent host reporting 8+ metric families walked off the page bottom, fought fpdf's auto page break, and scattered one orphan element per page (a 7-day delly report rendered 18 pages, ten of them near-blank). The grid now starts a new page before a row that will not fit; the same report renders 9 pages with intact cards. Verified live: real-mode delly report shows name, 288 data points, availability, charts, and correctly paginated cards. The underlying canonical-ID instability (raw store vs re-ingested view, and the resource_changes journal fragmenting across boot eras) is a separate root issue tracked for its own fix.
This commit is contained in:
parent
47392b5f68
commit
9bcf767b1b
4 changed files with 80 additions and 10 deletions
|
|
@ -352,6 +352,12 @@ func (h *ReportingHandlers) getReportingEnrichmentSnapshot(ctx context.Context,
|
|||
return emptyReportingEnrichmentSnapshot(), false
|
||||
}
|
||||
|
||||
// Resources must come from the canonical unified view (the same
|
||||
// re-ingested registry the UI and /api/state read), NOT the raw
|
||||
// resource store: the raw store's canonical IDs depend on per-boot
|
||||
// ingest order for merged-source hosts, so a UI-supplied resource ID
|
||||
// can be missing from it entirely.
|
||||
unifiedResources, _ := monitor.UnifiedResourceSnapshot()
|
||||
snapshot := reportingEnrichmentSnapshot{
|
||||
Nodes: monitor.NodesSnapshot(),
|
||||
VMs: monitor.VMsSnapshot(),
|
||||
|
|
@ -359,7 +365,7 @@ func (h *ReportingHandlers) getReportingEnrichmentSnapshot(ctx context.Context,
|
|||
ActiveAlerts: monitor.ActiveAlertsSnapshot(),
|
||||
RecentlyResolved: monitor.RecentlyResolvedSnapshot(),
|
||||
LegacyBackups: monitor.PVEBackupsSnapshot(),
|
||||
Resources: monitor.GetUnifiedResources(),
|
||||
Resources: unifiedResources,
|
||||
}
|
||||
snapshot.normalizeCollections()
|
||||
return snapshot, true
|
||||
|
|
|
|||
|
|
@ -4103,16 +4103,25 @@ func (m *Monitor) GetUnifiedResources() []unifiedresources.Resource {
|
|||
}
|
||||
|
||||
// MetricsTargetForResource resolves the metrics-store target for a canonical
|
||||
// unified resource ID via the wired resource store. The metrics store is
|
||||
// keyed by platform-native source IDs, and the target is computed on demand
|
||||
// by the resource registry rather than persisted on the Resource structs
|
||||
// GetUnifiedResources returns. Returns nil when no store is wired, the
|
||||
// store cannot resolve targets, or the resource has none.
|
||||
// unified resource ID. Resolution goes through the canonical unified view
|
||||
// (the same re-ingested registry the UI and /api/state read) because
|
||||
// callers hold IDs from that view: the raw resource store's canonical IDs
|
||||
// depend on per-boot ingest order for merged-source hosts, so the view and
|
||||
// the store can disagree about the same machine's ID. The raw store stays
|
||||
// as a fallback. Returns nil when nothing can resolve a target.
|
||||
func (m *Monitor) MetricsTargetForResource(resourceID string) *unifiedresources.MetricsTarget {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if view := m.GetUnifiedReadStateOrSnapshot(); view != nil {
|
||||
if resolver, ok := view.(MetricsTargetResourceStore); ok {
|
||||
if target := resolver.MetricsTargetForResource(resourceID); target != nil {
|
||||
return target
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
store := m.resourceStore
|
||||
m.mu.RUnlock()
|
||||
|
|
|
|||
|
|
@ -884,6 +884,9 @@ func (g *PDFGenerator) writeSummarySection(pdf *fpdf.Fpdf, data *ReportData) {
|
|||
startX := 20.0
|
||||
rowStartY := pdf.GetY()
|
||||
|
||||
_, pageHeight := pdf.GetPageSize()
|
||||
cardBottomLimit := pageHeight - 30
|
||||
|
||||
for i, metricType := range metricNames {
|
||||
stats := data.Summary.ByMetric[metricType]
|
||||
unit := GetMetricUnit(metricType)
|
||||
|
|
@ -898,6 +901,16 @@ func (g *PDFGenerator) writeSummarySection(pdf *fpdf.Fpdf, data *ReportData) {
|
|||
pdf.SetY(rowStartY)
|
||||
}
|
||||
|
||||
// The grid positions cards absolutely, so it must paginate
|
||||
// itself: a row that would cross the bottom margin previously
|
||||
// fought fpdf's auto page break and scattered one orphan
|
||||
// element per page for the rest of the section.
|
||||
if col == 0 && rowStartY+cardHeight > cardBottomLimit {
|
||||
pdf.AddPage()
|
||||
rowStartY = pdf.GetY()
|
||||
pdf.SetY(rowStartY)
|
||||
}
|
||||
|
||||
cardX := startX + float64(col)*(cardWidth+cardGap)
|
||||
cardY := rowStartY
|
||||
|
||||
|
|
@ -946,10 +959,9 @@ func (g *PDFGenerator) writeSummarySection(pdf *fpdf.Fpdf, data *ReportData) {
|
|||
writeStat(cardX+46, statsY+6, "Samples", fmt.Sprintf("%d", stats.Count))
|
||||
}
|
||||
|
||||
// Calculate final Y position based on number of rows
|
||||
numRows := (len(metricNames) + 1) / 2 // Round up
|
||||
finalY := rowStartY + float64(numRows)*(cardHeight+cardGap)
|
||||
pdf.SetY(finalY)
|
||||
// rowStartY tracks the current (possibly paginated) row, so the
|
||||
// section ends just below the last row of cards.
|
||||
pdf.SetY(rowStartY + cardHeight + cardGap)
|
||||
}
|
||||
|
||||
// writeChartsSection writes charts for each metric.
|
||||
|
|
|
|||
|
|
@ -467,3 +467,46 @@ func TestGenerateMulti_FlowsResourceBlocksOntoSharedPages(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSummarySection_PaginatesCardGridForManyMetrics pins the card grid's
|
||||
// self-pagination. Agent hosts report 8+ metric families; the absolutely
|
||||
// positioned grid previously walked off the page bottom and fought fpdf's
|
||||
// auto page break, scattering one orphan element per page for the rest of
|
||||
// the section (observed live as ten near-blank pages).
|
||||
func TestSummarySection_PaginatesCardGridForManyMetrics(t *testing.T) {
|
||||
byMetric := map[string]MetricStats{}
|
||||
for _, m := range []string{"cpu", "memory", "disk", "diskread", "diskwrite", "netin", "netout", "temperature", "iops", "usage"} {
|
||||
byMetric[m] = MetricStats{Min: 1, Max: 9, Avg: 5, Current: 5, Count: 100}
|
||||
}
|
||||
data := &ReportData{
|
||||
Title: "Many metrics",
|
||||
ResourceType: "agent",
|
||||
ResourceID: "agent-1",
|
||||
Resource: &ResourceInfo{Name: "host-01", Status: "online"},
|
||||
Start: time.Now().Add(-7 * 24 * time.Hour),
|
||||
End: time.Now(),
|
||||
GeneratedAt: time.Now(),
|
||||
Summary: MetricSummary{ByMetric: byMetric},
|
||||
TotalPoints: 1000,
|
||||
}
|
||||
|
||||
gen := NewPDFGenerator()
|
||||
out, err := gen.Generate(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate: %v", err)
|
||||
}
|
||||
text := extractPDFText(t, out)
|
||||
|
||||
pageTotal := regexp.MustCompile(`Page \d+ of (\d+)`).FindStringSubmatch(text)
|
||||
if pageTotal == nil {
|
||||
t.Fatalf("no page footer found in:\n%s", text)
|
||||
}
|
||||
if pages, _ := strconv.Atoi(pageTotal[1]); pages > 6 {
|
||||
t.Fatalf("10-metric report rendered %d pages; the card grid must paginate compactly", pages)
|
||||
}
|
||||
for _, label := range []string{"Disk Read", "Network Out", "Temperature"} {
|
||||
if !strings.Contains(text, label) {
|
||||
t.Fatalf("metric card %q missing after pagination:\n%s", label, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue