mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Drive reporting default ranges from catalog
This commit is contained in:
parent
2f2c650409
commit
3d85fde176
8 changed files with 121 additions and 34 deletions
|
|
@ -184,8 +184,9 @@ redefine reporting or inventory schema locally.
|
|||
That same API-owned performance-report definition also governs transport-side
|
||||
validation and attachment naming. Lifecycle-adjacent fleet surfaces may depend
|
||||
on those downloads, but they must treat allowed formats, multi-resource caps,
|
||||
optional metric/title support, and filename prefixes as API-owned reporting
|
||||
contract rather than mirroring local constants.
|
||||
optional metric/title support, default fallback range windows, and filename
|
||||
prefixes as API-owned reporting contract rather than mirroring local
|
||||
constants.
|
||||
That adjacent export contract now also carries canonical Proxmox pool
|
||||
membership for each VM row. Lifecycle-adjacent install and fleet surfaces may
|
||||
reuse those current-state facts, but they must still treat the pool column as
|
||||
|
|
|
|||
|
|
@ -234,8 +234,9 @@ emit unsupported metric-filter or custom-title fields from local assumptions.
|
|||
The same performance-report catalog definition also owns backend transport
|
||||
validation and download semantics. `internal/api/metrics_reporting_handlers.go`
|
||||
must derive allowed formats, default format selection, multi-resource limits,
|
||||
optional metric/title field emission, and attachment filename prefixes from the
|
||||
canonical reporting definition instead of hardcoding a second local contract.
|
||||
optional metric/title field emission, default fallback range window, and
|
||||
attachment filename prefixes from the canonical reporting definition instead of
|
||||
hardcoding a second local contract.
|
||||
The `/api/resources` serializer now also refreshes canonical identity and
|
||||
policy metadata through the shared unified-resource helper before it writes
|
||||
the payload, so backend and frontend contract tests stay aligned on one
|
||||
|
|
|
|||
|
|
@ -230,8 +230,8 @@ inventory column contract.
|
|||
That same API-owned performance-report definition also governs transport-side
|
||||
validation and attachment naming. Storage and recovery flows may consume those
|
||||
downloads, but they must treat allowed formats, multi-resource caps, optional
|
||||
metric/title support, and filename prefixes as API/reporting contract rather
|
||||
than rebuilding local reporting constants.
|
||||
metric/title support, default fallback range windows, and filename prefixes as
|
||||
API/reporting contract rather than rebuilding local reporting constants.
|
||||
That adjacent export contract now also includes canonical Proxmox pool
|
||||
membership for each VM row. Storage and recovery flows may use those current-
|
||||
state facts when they need fleet context, but they must consume the API-owned
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
|
@ -498,6 +499,35 @@ func TestContract_PerformanceReportTransportUsesCatalogDefinition(t *testing.T)
|
|||
}
|
||||
}
|
||||
|
||||
func TestContract_PerformanceReportTransportUsesCatalogDefaultRange(t *testing.T) {
|
||||
engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"}
|
||||
original := reporting.GetEngine()
|
||||
reporting.SetEngine(engine)
|
||||
t.Cleanup(func() { reporting.SetEngine(original) })
|
||||
|
||||
handler := NewReportingHandlers(nil, nil)
|
||||
end := time.Date(2026, 3, 25, 15, 0, 0, 0, time.UTC)
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
"/api/reporting?resourceType=node&resourceId=node-1&end="+url.QueryEscape(end.Format(time.RFC3339)),
|
||||
nil,
|
||||
)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.HandleGenerateReport(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
definition := reporting.DescribePerformanceReport()
|
||||
if got := engine.lastReq.Start; !got.Equal(end.Add(-definition.DefaultRangeDuration())) {
|
||||
t.Fatalf("expected canonical default start time, got %s", got)
|
||||
}
|
||||
if !engine.lastReq.End.Equal(end) {
|
||||
t.Fatalf("expected canonical end time, got %s", engine.lastReq.End)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContract_VMInventoryExportDefinitionJSONSnapshot(t *testing.T) {
|
||||
handler := NewReportingHandlers(nil, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/inventory/vms/definition", nil)
|
||||
|
|
|
|||
|
|
@ -75,6 +75,29 @@ func normalizePerformanceReportOptionalFields(
|
|||
return normalizedMetricType, normalizedTitle
|
||||
}
|
||||
|
||||
func normalizePerformanceReportTimeRange(
|
||||
definition reporting.PerformanceReportDefinition,
|
||||
startRaw string,
|
||||
endRaw string,
|
||||
now time.Time,
|
||||
) (time.Time, time.Time) {
|
||||
end := now
|
||||
if strings.TrimSpace(endRaw) != "" {
|
||||
if parsed, err := time.Parse(time.RFC3339, endRaw); err == nil {
|
||||
end = parsed
|
||||
}
|
||||
}
|
||||
|
||||
start := end.Add(-definition.DefaultRangeDuration())
|
||||
if strings.TrimSpace(startRaw) != "" {
|
||||
if parsed, err := time.Parse(time.RFC3339, startRaw); err == nil {
|
||||
start = parsed
|
||||
}
|
||||
}
|
||||
|
||||
return start, end
|
||||
}
|
||||
|
||||
type reportingEnrichmentSnapshot struct {
|
||||
Nodes []models.Node
|
||||
VMs []models.VM
|
||||
|
|
@ -326,20 +349,12 @@ func (h *ReportingHandlers) HandleGenerateReport(w http.ResponseWriter, r *http.
|
|||
q.Get("title"),
|
||||
)
|
||||
|
||||
// Parse range
|
||||
end := time.Now()
|
||||
if q.Get("end") != "" {
|
||||
if t, err := time.Parse(time.RFC3339, q.Get("end")); err == nil {
|
||||
end = t
|
||||
}
|
||||
}
|
||||
|
||||
start := end.Add(-24 * time.Hour)
|
||||
if q.Get("start") != "" {
|
||||
if t, err := time.Parse(time.RFC3339, q.Get("start")); err == nil {
|
||||
start = t
|
||||
}
|
||||
}
|
||||
start, end := normalizePerformanceReportTimeRange(
|
||||
definition,
|
||||
q.Get("start"),
|
||||
q.Get("end"),
|
||||
time.Now(),
|
||||
)
|
||||
|
||||
req := reporting.MetricReportRequest{
|
||||
ResourceType: resourceType,
|
||||
|
|
@ -671,19 +686,7 @@ func (h *ReportingHandlers) HandleGenerateMultiReport(w http.ResponseWriter, r *
|
|||
}
|
||||
metricType, title := normalizePerformanceReportOptionalFields(definition, body.MetricType, body.Title)
|
||||
|
||||
// Parse time range
|
||||
end := time.Now()
|
||||
if body.End != "" {
|
||||
if t, err := time.Parse(time.RFC3339, body.End); err == nil {
|
||||
end = t
|
||||
}
|
||||
}
|
||||
start := end.Add(-24 * time.Hour)
|
||||
if body.Start != "" {
|
||||
if t, err := time.Parse(time.RFC3339, body.Start); err == nil {
|
||||
start = t
|
||||
}
|
||||
}
|
||||
start, end := normalizePerformanceReportTimeRange(definition, body.Start, body.End, time.Now())
|
||||
|
||||
// Build multi-report request
|
||||
multiReq := reporting.MultiReportRequest{
|
||||
|
|
|
|||
|
|
@ -417,6 +417,34 @@ func TestReportingHandlers_GenerateMultiReport_TrimsOptionalFieldsAndUsesCanonic
|
|||
}
|
||||
}
|
||||
|
||||
func TestReportingHandlers_GenerateReport_UsesCatalogDefaultRangeDuration(t *testing.T) {
|
||||
engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"}
|
||||
original := reporting.GetEngine()
|
||||
reporting.SetEngine(engine)
|
||||
t.Cleanup(func() { reporting.SetEngine(original) })
|
||||
|
||||
handler := NewReportingHandlers(nil, nil)
|
||||
end := time.Date(2026, 3, 25, 15, 0, 0, 0, time.UTC)
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
"/api/reporting?format=pdf&resourceType=node&resourceId=node-1&end="+url.QueryEscape(end.Format(time.RFC3339)),
|
||||
nil,
|
||||
)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.HandleGenerateReport(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, rr.Code, rr.Body.String())
|
||||
}
|
||||
if got := engine.lastReq.Start; !got.Equal(end.Add(-reporting.DescribePerformanceReport().DefaultRangeDuration())) {
|
||||
t.Fatalf("expected canonical default start, got %s", got)
|
||||
}
|
||||
if !engine.lastReq.End.Equal(end) {
|
||||
t.Fatalf("expected canonical end time, got %s", engine.lastReq.End)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportingHandlers_ExportVMInventory_EmptySnapshotStillReturnsCSVHeader(t *testing.T) {
|
||||
handler := NewReportingHandlers(nil, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/inventory/vms/export", nil)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package reporting
|
||||
|
||||
import "time"
|
||||
|
||||
// ReportingFormatDefinition describes one supported output format for an
|
||||
// operator-facing reporting surface.
|
||||
type ReportingFormatDefinition struct {
|
||||
|
|
@ -56,6 +58,17 @@ func (d PerformanceReportDefinition) SupportsFormat(format ReportFormat) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// DefaultRangeDuration returns the canonical fallback time window for the
|
||||
// performance reporting surface.
|
||||
func (d PerformanceReportDefinition) DefaultRangeDuration() time.Duration {
|
||||
for _, candidate := range d.Ranges {
|
||||
if candidate.Key == d.DefaultRange {
|
||||
return time.Duration(candidate.WindowHours) * time.Hour
|
||||
}
|
||||
}
|
||||
return 24 * time.Hour
|
||||
}
|
||||
|
||||
// DescribePerformanceReport returns the canonical definition for Pulse's
|
||||
// historical performance reporting surface.
|
||||
func DescribePerformanceReport() PerformanceReportDefinition {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package reporting
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDescribeReportingCatalog_DefinesCanonicalSurfaces(t *testing.T) {
|
||||
catalog := DescribeReportingCatalog()
|
||||
|
|
@ -35,3 +38,11 @@ func TestPerformanceReportDefinition_SupportsFormat(t *testing.T) {
|
|||
t.Fatalf("did not expect xlsx to be supported")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformanceReportDefinition_DefaultRangeDuration(t *testing.T) {
|
||||
definition := DescribePerformanceReport()
|
||||
|
||||
if got := definition.DefaultRangeDuration(); got != 24*time.Hour {
|
||||
t.Fatalf("default range duration = %s, want %s", got, 24*time.Hour)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue