diff --git a/.drone.yml b/.drone.yml index 82112ab..ca606ae 100644 --- a/.drone.yml +++ b/.drone.yml @@ -56,7 +56,7 @@ steps: commands: - cp /shared/vctp-linux-amd64 ./build/vctp-linux-amd64 #- find . - - nfpm package --config vctp.yml --packager rpm --target ./build/ + - nfpm package --config vctp-service.yml --packager rpm --target ./build/ - ls -lah ./build/ - name: dell-sftp-deploy diff --git a/README.md b/README.md index 8ba9b74..1970db7 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,13 @@ use: vctp -settings /path/to/vctp.yml -db-cleanup ``` +If you want a one-time cache backfill for the vCenter totals cache tables +(`vcenter_latest_totals` and `vcenter_aggregate_totals`) and exit, use: + +```shell +vctp -settings /path/to/vctp.yml -backfill-vcenter-cache +``` + ## Database Configuration By default the app uses SQLite and creates/opens `db.sqlite3`. diff --git a/components/views/snapshots.templ b/components/views/snapshots.templ index 047de1d..596a89f 100644 --- a/components/views/snapshots.templ +++ b/components/views/snapshots.templ @@ -23,14 +23,12 @@ type VcenterTotalsEntry struct { } type VcenterTotalsMeta struct { - ViewType string - TypeLabel string - HourlyLink string - DailyLink string - MonthlyLink string - HourlyClass string - DailyClass string - MonthlyClass string + ViewType string + TypeLabel string + HourlyLink string + DailyLink string + HourlyClass string + DailyClass string } type VcenterChartData struct { @@ -176,12 +174,11 @@ templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcen Dashboard -
- Hourly - Daily - Monthly -
- +
+ Hourly Detail (45d) + Daily Aggregated +
+

{ meta.TypeLabel } Snapshots

diff --git a/components/views/snapshots_templ.go b/components/views/snapshots_templ.go index 926ff43..fb88a08 100644 --- a/components/views/snapshots_templ.go +++ b/components/views/snapshots_templ.go @@ -31,14 +31,12 @@ type VcenterTotalsEntry struct { } type VcenterTotalsMeta struct { - ViewType string - TypeLabel string - HourlyLink string - DailyLink string - MonthlyLink string - HourlyClass string - DailyClass string - MonthlyClass string + ViewType string + TypeLabel string + HourlyLink string + DailyLink string + HourlyClass string + DailyClass string } type VcenterChartData struct { @@ -168,7 +166,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 62, Col: 50} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 60, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -181,7 +179,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 63, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 61, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -194,7 +192,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 71, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 69, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -213,7 +211,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 86, Col: 77} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 84, Col: 77} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -231,7 +229,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 92, Col: 76} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 90, Col: 76} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -244,7 +242,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 96, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 94, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -257,7 +255,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var11 templ.SafeURL templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 99, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 97, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -320,7 +318,7 @@ func VcenterList(links []VcenterLink) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(len(links)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 132, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 130, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -338,7 +336,7 @@ func VcenterList(links []VcenterLink) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 145, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 143, Col: 62} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -351,7 +349,7 @@ func VcenterList(links []VcenterLink) templ.Component { var templ_7745c5c3_Var15 templ.SafeURL templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 147, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 145, Col: 48} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -414,7 +412,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vcenter) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 171, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 169, Col: 63} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { @@ -427,7 +425,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 172, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 170, Col: 62} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -462,13 +460,13 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var21 templ.SafeURL templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(meta.HourlyLink) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 180, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 178, Col: 59} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">Hourly ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">Hourly Detail (45d) ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -497,159 +495,124 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var24 templ.SafeURL templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(meta.DailyLink) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 181, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 179, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">Daily ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">Daily Aggregated

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var25 = []any{meta.MonthlyClass} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var25...) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 184, Col: 56} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var25).String()) + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 1, Col: 0} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 185, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" href=\"") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var27 templ.SafeURL - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(meta.MonthlyLink) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 182, Col: 60} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">Monthly

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 187, Col: 56} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " Snapshots

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 188, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " records
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " records") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if chart.ConfigJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, entry := range entries { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
Snapshot TimeVMsvCPUsRAM (GB)
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var28 string + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 214, Col: 30} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 215, Col: 48} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON) + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 193, Col: 145} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 216, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, entry := range entries { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
Snapshot TimeVMsvCPUsRAM (GB)
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot) + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 218, Col: 48} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var33 string - templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 219, Col: 50} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var34 string - templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 220, Col: 51} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -657,7 +620,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/db/helpers.go b/db/helpers.go index 8835d27..429bb93 100644 --- a/db/helpers.go +++ b/db/helpers.go @@ -1121,6 +1121,277 @@ CREATE TABLE IF NOT EXISTS vcenter_totals ( return nil } +// EnsureVcenterLatestTotalsTable creates a compact table with one latest totals row per vCenter. +func EnsureVcenterLatestTotalsTable(ctx context.Context, dbConn *sqlx.DB) error { + driver := strings.ToLower(dbConn.DriverName()) + var ddl string + switch driver { + case "pgx", "postgres": + ddl = ` +CREATE TABLE IF NOT EXISTS vcenter_latest_totals ( + "Vcenter" TEXT PRIMARY KEY, + "SnapshotTime" BIGINT NOT NULL, + "VmCount" BIGINT NOT NULL, + "VcpuTotal" BIGINT NOT NULL, + "RamTotalGB" BIGINT NOT NULL +);` + default: + ddl = ` +CREATE TABLE IF NOT EXISTS vcenter_latest_totals ( + "Vcenter" TEXT PRIMARY KEY, + "SnapshotTime" BIGINT NOT NULL, + "VmCount" BIGINT NOT NULL, + "VcpuTotal" BIGINT NOT NULL, + "RamTotalGB" BIGINT NOT NULL +);` + } + if _, err := execLog(ctx, dbConn, ddl); err != nil { + return err + } + return nil +} + +// UpsertVcenterLatestTotals stores the latest totals per vCenter. +func UpsertVcenterLatestTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotTime int64, vmCount, vcpuTotal, ramTotal int64) error { + if strings.TrimSpace(vcenter) == "" { + return fmt.Errorf("vcenter is empty") + } + if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil { + return err + } + _, err := execLog(ctx, dbConn, ` +INSERT INTO vcenter_latest_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB") +VALUES ($1,$2,$3,$4,$5) +ON CONFLICT ("Vcenter") DO UPDATE SET + "SnapshotTime" = EXCLUDED."SnapshotTime", + "VmCount" = EXCLUDED."VmCount", + "VcpuTotal" = EXCLUDED."VcpuTotal", + "RamTotalGB" = EXCLUDED."RamTotalGB" +WHERE EXCLUDED."SnapshotTime" >= vcenter_latest_totals."SnapshotTime" +`, vcenter, snapshotTime, vmCount, vcpuTotal, ramTotal) + return err +} + +// EnsureVcenterAggregateTotalsTable creates a compact cache for hourly/daily (and optional monthly) totals. +func EnsureVcenterAggregateTotalsTable(ctx context.Context, dbConn *sqlx.DB) error { + ddl := ` +CREATE TABLE IF NOT EXISTS vcenter_aggregate_totals ( + "SnapshotType" TEXT NOT NULL, + "Vcenter" TEXT NOT NULL, + "SnapshotTime" BIGINT NOT NULL, + "VmCount" BIGINT NOT NULL, + "VcpuTotal" BIGINT NOT NULL, + "RamTotalGB" BIGINT NOT NULL, + PRIMARY KEY ("SnapshotType","Vcenter","SnapshotTime") +);` + if _, err := execLog(ctx, dbConn, ddl); err != nil { + return err + } + _, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vcenter_aggregate_totals_vc_type_time_idx ON vcenter_aggregate_totals ("Vcenter","SnapshotType","SnapshotTime" DESC)`) + return nil +} + +// UpsertVcenterAggregateTotal stores per-vCenter totals for a snapshot type/time. +func UpsertVcenterAggregateTotal(ctx context.Context, dbConn *sqlx.DB, snapshotType, vcenter string, snapshotTime int64, vmCount, vcpuTotal, ramTotal int64) error { + snapshotType = strings.ToLower(strings.TrimSpace(snapshotType)) + if snapshotType == "" { + return fmt.Errorf("snapshot type is empty") + } + if strings.TrimSpace(vcenter) == "" { + return fmt.Errorf("vcenter is empty") + } + if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil { + return err + } + _, err := execLog(ctx, dbConn, ` +INSERT INTO vcenter_aggregate_totals ("SnapshotType","Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB") +VALUES ($1,$2,$3,$4,$5,$6) +ON CONFLICT ("SnapshotType","Vcenter","SnapshotTime") DO UPDATE SET + "VmCount" = EXCLUDED."VmCount", + "VcpuTotal" = EXCLUDED."VcpuTotal", + "RamTotalGB" = EXCLUDED."RamTotalGB" +`, snapshotType, vcenter, snapshotTime, vmCount, vcpuTotal, ramTotal) + return err +} + +// ReplaceVcenterAggregateTotalsFromSummary recomputes one snapshot's per-vCenter totals from a summary table. +func ReplaceVcenterAggregateTotalsFromSummary(ctx context.Context, dbConn *sqlx.DB, summaryTable, snapshotType string, snapshotTime int64) (int, error) { + if err := ValidateTableName(summaryTable); err != nil { + return 0, err + } + snapshotType = strings.ToLower(strings.TrimSpace(snapshotType)) + if snapshotType == "" { + return 0, fmt.Errorf("snapshot type is empty") + } + if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil { + return 0, err + } + if _, err := execLog(ctx, dbConn, ` +DELETE FROM vcenter_aggregate_totals +WHERE "SnapshotType" = $1 AND "SnapshotTime" = $2 +`, snapshotType, snapshotTime); err != nil { + return 0, err + } + query := fmt.Sprintf(` +SELECT + "Vcenter" AS vcenter, + COUNT(1) AS vm_count, + CAST(COALESCE(SUM(COALESCE("AvgVcpuCount","VcpuCount")),0) AS BIGINT) AS vcpu_total, + CAST(COALESCE(SUM(COALESCE("AvgRamGB","RamGB")),0) AS BIGINT) AS ram_total +FROM %s +GROUP BY "Vcenter" +`, summaryTable) + var rows []struct { + Vcenter string `db:"vcenter"` + VmCount int64 `db:"vm_count"` + VcpuTotal int64 `db:"vcpu_total"` + RamTotal int64 `db:"ram_total"` + } + if err := selectLog(ctx, dbConn, &rows, query); err != nil { + return 0, err + } + upserted := 0 + for _, row := range rows { + if err := UpsertVcenterAggregateTotal(ctx, dbConn, snapshotType, row.Vcenter, snapshotTime, row.VmCount, row.VcpuTotal, row.RamTotal); err != nil { + return upserted, err + } + upserted++ + } + return upserted, nil +} + +// SyncVcenterAggregateTotalsFromRegistry refreshes cached totals for summary snapshots listed in snapshot_registry. +func SyncVcenterAggregateTotalsFromRegistry(ctx context.Context, dbConn *sqlx.DB, snapshotType string) (int, int, error) { + snapshotType = strings.ToLower(strings.TrimSpace(snapshotType)) + if snapshotType == "" { + return 0, 0, fmt.Errorf("snapshot type is empty") + } + if snapshotType != "daily" && snapshotType != "monthly" { + return 0, 0, fmt.Errorf("unsupported snapshot type for summary cache sync: %s", snapshotType) + } + if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil { + return 0, 0, err + } + + query := dbConn.Rebind(` +SELECT table_name, snapshot_time +FROM snapshot_registry +WHERE snapshot_type = ? +ORDER BY snapshot_time +`) + var snapshots []struct { + TableName string `db:"table_name"` + SnapshotTime int64 `db:"snapshot_time"` + } + if err := selectLog(ctx, dbConn, &snapshots, query, snapshotType); err != nil { + return 0, 0, err + } + + snapshotsRefreshed := 0 + rowsUpserted := 0 + failures := 0 + for _, snapshot := range snapshots { + if err := ValidateTableName(snapshot.TableName); err != nil { + failures++ + slog.Warn("skipping invalid summary table in snapshot registry", "snapshot_type", snapshotType, "table", snapshot.TableName, "error", err) + continue + } + upserted, err := ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, snapshot.TableName, snapshotType, snapshot.SnapshotTime) + if err != nil { + failures++ + slog.Warn("failed to refresh vcenter aggregate cache from summary table", "snapshot_type", snapshotType, "table", snapshot.TableName, "snapshot_time", snapshot.SnapshotTime, "error", err) + continue + } + snapshotsRefreshed++ + rowsUpserted += upserted + } + + if failures > 0 { + return snapshotsRefreshed, rowsUpserted, fmt.Errorf("vcenter aggregate cache sync finished with %d failed snapshot(s)", failures) + } + return snapshotsRefreshed, rowsUpserted, nil +} + +// ListVcenterAggregateTotals lists cached totals by type. +func ListVcenterAggregateTotals(ctx context.Context, dbConn *sqlx.DB, vcenter, snapshotType string, limit int) ([]VcenterTotalRow, error) { + snapshotType = strings.ToLower(strings.TrimSpace(snapshotType)) + if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil { + return nil, err + } + if limit <= 0 { + limit = 200 + } + rows := make([]VcenterTotalRow, 0, limit) + query := ` +SELECT "Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB" +FROM vcenter_aggregate_totals +WHERE "Vcenter" = $1 AND "SnapshotType" = $2 +ORDER BY "SnapshotTime" DESC +LIMIT $3 +` + if err := selectLog(ctx, dbConn, &rows, query, vcenter, snapshotType, limit); err != nil { + return nil, err + } + return rows, nil +} + +// ListVcenterAggregateTotalsSince lists cached totals by type from a lower-bound timestamp. +func ListVcenterAggregateTotalsSince(ctx context.Context, dbConn *sqlx.DB, vcenter, snapshotType string, since time.Time) ([]VcenterTotalRow, error) { + snapshotType = strings.ToLower(strings.TrimSpace(snapshotType)) + if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil { + return nil, err + } + rows := make([]VcenterTotalRow, 0, 256) + query := ` +SELECT "Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB" +FROM vcenter_aggregate_totals +WHERE "Vcenter" = $1 AND "SnapshotType" = $2 AND "SnapshotTime" >= $3 +ORDER BY "SnapshotTime" DESC +` + if err := selectLog(ctx, dbConn, &rows, query, vcenter, snapshotType, since.Unix()); err != nil { + return nil, err + } + return rows, nil +} + +// SyncVcenterLatestTotalsFromHistory backfills latest totals from existing vcenter_totals history. +func SyncVcenterLatestTotalsFromHistory(ctx context.Context, dbConn *sqlx.DB) (int, error) { + if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil { + return 0, err + } + if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil { + return 0, err + } + var rows []struct { + Vcenter string `db:"Vcenter"` + SnapshotTime int64 `db:"SnapshotTime"` + VmCount int64 `db:"VmCount"` + VcpuTotal int64 `db:"VcpuTotal"` + RamTotalGB int64 `db:"RamTotalGB"` + } + if err := selectLog(ctx, dbConn, &rows, ` +SELECT t."Vcenter", t."SnapshotTime", t."VmCount", t."VcpuTotal", t."RamTotalGB" +FROM vcenter_totals t +JOIN ( + SELECT "Vcenter", MAX("SnapshotTime") AS max_snapshot_time + FROM vcenter_totals + GROUP BY "Vcenter" +) latest + ON latest."Vcenter" = t."Vcenter" + AND latest.max_snapshot_time = t."SnapshotTime" +`); err != nil { + return 0, err + } + upserted := 0 + for _, row := range rows { + if err := UpsertVcenterLatestTotals(ctx, dbConn, row.Vcenter, row.SnapshotTime, row.VmCount, row.VcpuTotal, row.RamTotalGB); err != nil { + return upserted, err + } + upserted++ + } + return upserted, nil +} + // InsertVcenterTotals records totals for a vcenter at a snapshot time. func InsertVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotTime time.Time, vmCount, vcpuTotal, ramTotal int64) error { if strings.TrimSpace(vcenter) == "" { @@ -1129,15 +1400,56 @@ func InsertVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, s if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil { return err } + if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil { + return err + } _, err := execLog(ctx, dbConn, ` INSERT INTO vcenter_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB") VALUES ($1,$2,$3,$4,$5) `, vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal) - return err + if err != nil { + return err + } + if err := UpsertVcenterLatestTotals(ctx, dbConn, vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal); err != nil { + return err + } + if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal); err != nil { + slog.Warn("failed to upsert vcenter_aggregate_totals", "snapshot_type", "hourly", "vcenter", vcenter, "snapshot_time", snapshotTime.Unix(), "error", err) + } + return nil } // ListVcenters returns distinct vcenter URLs tracked. func ListVcenters(ctx context.Context, dbConn *sqlx.DB) ([]string, error) { + if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err == nil { + rows, err := dbConn.QueryxContext(ctx, `SELECT "Vcenter" FROM vcenter_latest_totals ORDER BY "Vcenter"`) + if err == nil { + defer rows.Close() + out := make([]string, 0, 32) + for rows.Next() { + var v string + if err := rows.Scan(&v); err != nil { + return nil, err + } + out = append(out, v) + } + if err := rows.Err(); err != nil { + return nil, err + } + if len(out) > 0 { + return out, nil + } + // Older installs may have vcenter_totals populated but no latest cache yet. + if _, err := SyncVcenterLatestTotalsFromHistory(ctx, dbConn); err == nil { + refreshed := make([]string, 0, 32) + if err := selectLog(ctx, dbConn, &refreshed, `SELECT "Vcenter" FROM vcenter_latest_totals ORDER BY "Vcenter"`); err == nil && len(refreshed) > 0 { + return refreshed, nil + } + } + } + } + + // Fallback for older DBs before vcenter_latest_totals gets populated. if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil { return nil, err } @@ -1187,15 +1499,66 @@ LIMIT $2` return rows, nil } +// ListVcenterHourlyTotalsSince returns hourly totals for a vCenter from a minimum snapshot time. +func ListVcenterHourlyTotalsSince(ctx context.Context, dbConn *sqlx.DB, vcenter string, since time.Time) ([]VcenterTotalRow, error) { + cachedRows, cacheErr := ListVcenterAggregateTotalsSince(ctx, dbConn, vcenter, "hourly", since) + if cacheErr == nil && len(cachedRows) > 0 { + return cachedRows, nil + } + if cacheErr != nil { + slog.Warn("failed to read hourly totals cache", "vcenter", vcenter, "since", since.Unix(), "error", cacheErr) + } + + if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil { + return nil, err + } + rows := make([]VcenterTotalRow, 0, 256) + query := ` +SELECT "Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB" +FROM vcenter_totals +WHERE "Vcenter" = $1 + AND "SnapshotTime" >= $2 +ORDER BY "SnapshotTime" DESC` + if err := selectLog(ctx, dbConn, &rows, query, vcenter, since.Unix()); err != nil { + return nil, err + } + for _, row := range rows { + if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", row.Vcenter, row.SnapshotTime, row.VmCount, row.VcpuTotal, row.RamTotalGB); err != nil { + slog.Warn("failed to warm hourly totals cache", "vcenter", row.Vcenter, "snapshot_time", row.SnapshotTime, "error", err) + break + } + } + return rows, nil +} + // ListVcenterTotalsByType returns totals for a vcenter for the requested snapshot type (hourly, daily, monthly). -// Hourly values come from vcenter_totals; daily/monthly are derived from the summary tables referenced in snapshot_registry. +// Prefer vcenter_aggregate_totals cache and fallback to source tables when cache is empty. func ListVcenterTotalsByType(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotType string, limit int) ([]VcenterTotalRow, error) { - snapshotType = strings.ToLower(snapshotType) + snapshotType = strings.ToLower(strings.TrimSpace(snapshotType)) if snapshotType == "" { snapshotType = "hourly" } + + cachedRows, cacheErr := ListVcenterAggregateTotals(ctx, dbConn, vcenter, snapshotType, limit) + if cacheErr == nil && len(cachedRows) > 0 { + return cachedRows, nil + } + if cacheErr != nil { + slog.Warn("failed to read vcenter aggregate totals cache", "snapshot_type", snapshotType, "vcenter", vcenter, "error", cacheErr) + } + if snapshotType == "hourly" { - return ListVcenterTotals(ctx, dbConn, vcenter, limit) + rows, err := ListVcenterTotals(ctx, dbConn, vcenter, limit) + if err != nil { + return nil, err + } + for _, row := range rows { + if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", row.Vcenter, row.SnapshotTime, row.VmCount, row.VcpuTotal, row.RamTotalGB); err != nil { + slog.Warn("failed to warm hourly totals cache", "vcenter", row.Vcenter, "snapshot_time", row.SnapshotTime, "error", err) + break + } + } + return rows, nil } if limit <= 0 { @@ -1240,6 +1603,12 @@ LIMIT $2 RamTotalGB: agg.RamTotalGB, }) } + for _, row := range out { + if err := UpsertVcenterAggregateTotal(ctx, dbConn, snapshotType, row.Vcenter, row.SnapshotTime, row.VmCount, row.VcpuTotal, row.RamTotalGB); err != nil { + slog.Warn("failed to warm vcenter aggregate totals cache", "snapshot_type", snapshotType, "vcenter", row.Vcenter, "snapshot_time", row.SnapshotTime, "error", err) + break + } + } return out, nil } @@ -1589,6 +1958,9 @@ func SyncVcenterTotalsFromSnapshots(ctx context.Context, dbConn *sqlx.DB) error if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil { return err } + if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil { + return err + } driver := strings.ToLower(dbConn.DriverName()) var hourlyTables []struct { TableName string `db:"table_name"` @@ -1640,6 +2012,12 @@ WHERE NOT EXISTS ( if _, err := execLog(ctx, dbConn, insert, a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal); err != nil { slog.Warn("failed to backfill vcenter_totals", "table", ht.TableName, "vcenter", a.Vcenter, "snapshot_time", ht.SnapshotTime, "error", err) } + if err := UpsertVcenterLatestTotals(ctx, dbConn, a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal); err != nil { + slog.Warn("failed to upsert vcenter_latest_totals", "table", ht.TableName, "vcenter", a.Vcenter, "snapshot_time", ht.SnapshotTime, "error", err) + } + if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal); err != nil { + slog.Warn("failed to upsert vcenter_aggregate_totals", "table", ht.TableName, "snapshot_type", "hourly", "vcenter", a.Vcenter, "snapshot_time", ht.SnapshotTime, "error", err) + } } } return nil diff --git a/db/helpers_cache_and_index_test.go b/db/helpers_cache_and_index_test.go index 2fd57bf..f8b408e 100644 --- a/db/helpers_cache_and_index_test.go +++ b/db/helpers_cache_and_index_test.go @@ -162,3 +162,316 @@ func TestParseHourlySnapshotUnix(t *testing.T) { } } } + +func TestVcenterLatestTotalsAndListFallback(t *testing.T) { + ctx := context.Background() + dbConn := newTestSQLiteDB(t) + + if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil { + t.Fatalf("failed to ensure vcenter_latest_totals: %v", err) + } + if err := InsertVcenterTotals(ctx, dbConn, "vc-a", time.Unix(200, 0), 10, 20, 30); err != nil { + t.Fatalf("failed to insert totals for vc-a: %v", err) + } + // Older snapshot should not replace latest totals. + if err := InsertVcenterTotals(ctx, dbConn, "vc-a", time.Unix(100, 0), 1, 2, 3); err != nil { + t.Fatalf("failed to insert older totals for vc-a: %v", err) + } + if err := InsertVcenterTotals(ctx, dbConn, "vc-b", time.Unix(300, 0), 11, 21, 31); err != nil { + t.Fatalf("failed to insert totals for vc-b: %v", err) + } + + vcenters, err := ListVcenters(ctx, dbConn) + if err != nil { + t.Fatalf("ListVcenters failed: %v", err) + } + if len(vcenters) != 2 || vcenters[0] != "vc-a" || vcenters[1] != "vc-b" { + t.Fatalf("unexpected vcenter list: %#v", vcenters) + } + + var latest struct { + SnapshotTime int64 `db:"SnapshotTime"` + VmCount int64 `db:"VmCount"` + VcpuTotal int64 `db:"VcpuTotal"` + RamTotalGB int64 `db:"RamTotalGB"` + } + if err := dbConn.GetContext(ctx, &latest, ` +SELECT "SnapshotTime","VmCount","VcpuTotal","RamTotalGB" +FROM vcenter_latest_totals +WHERE "Vcenter" = ? +`, "vc-a"); err != nil { + t.Fatalf("failed to query latest totals for vc-a: %v", err) + } + if latest.SnapshotTime != 200 || latest.VmCount != 10 || latest.VcpuTotal != 20 || latest.RamTotalGB != 30 { + t.Fatalf("unexpected latest totals for vc-a: %#v", latest) + } +} + +func TestListVcenterHourlyTotalsSince(t *testing.T) { + ctx := context.Background() + dbConn := newTestSQLiteDB(t) + + base := time.Unix(1_700_000_000, 0) + if err := InsertVcenterTotals(ctx, dbConn, "vc-a", base.AddDate(0, 0, -60), 1, 2, 3); err != nil { + t.Fatalf("failed to insert old totals: %v", err) + } + if err := InsertVcenterTotals(ctx, dbConn, "vc-a", base.AddDate(0, 0, -10), 10, 20, 30); err != nil { + t.Fatalf("failed to insert recent totals: %v", err) + } + if err := InsertVcenterTotals(ctx, dbConn, "vc-b", base.AddDate(0, 0, -5), 100, 200, 300); err != nil { + t.Fatalf("failed to insert other-vcenter totals: %v", err) + } + + rows, err := ListVcenterHourlyTotalsSince(ctx, dbConn, "vc-a", base.AddDate(0, 0, -45)) + if err != nil { + t.Fatalf("ListVcenterHourlyTotalsSince failed: %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 row for vc-a since cutoff, got %d", len(rows)) + } + if rows[0].SnapshotTime != base.AddDate(0, 0, -10).Unix() || rows[0].VmCount != 10 { + t.Fatalf("unexpected row returned: %#v", rows[0]) + } +} + +func TestInsertVcenterTotalsUpsertsHourlyAggregate(t *testing.T) { + ctx := context.Background() + dbConn := newTestSQLiteDB(t) + + snapshotTime := time.Unix(1_700_000_500, 0) + if err := InsertVcenterTotals(ctx, dbConn, "vc-a", snapshotTime, 12, 24, 48); err != nil { + t.Fatalf("InsertVcenterTotals failed: %v", err) + } + + rows, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "hourly", 10) + if err != nil { + t.Fatalf("ListVcenterAggregateTotals failed: %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 hourly aggregate row, got %d", len(rows)) + } + if rows[0].SnapshotTime != snapshotTime.Unix() || rows[0].VmCount != 12 || rows[0].VcpuTotal != 24 || rows[0].RamTotalGB != 48 { + t.Fatalf("unexpected hourly aggregate row: %#v", rows[0]) + } +} + +func TestListVcenterHourlyTotalsSinceUsesAggregateCache(t *testing.T) { + ctx := context.Background() + dbConn := newTestSQLiteDB(t) + + base := time.Unix(1_700_000_000, 0) + if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", "vc-a", base.Unix(), 7, 14, 21); err != nil { + t.Fatalf("UpsertVcenterAggregateTotal failed: %v", err) + } + + rows, err := ListVcenterHourlyTotalsSince(ctx, dbConn, "vc-a", base.Add(-24*time.Hour)) + if err != nil { + t.Fatalf("ListVcenterHourlyTotalsSince failed: %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 cached row, got %d", len(rows)) + } + if rows[0].SnapshotTime != base.Unix() || rows[0].VmCount != 7 || rows[0].VcpuTotal != 14 || rows[0].RamTotalGB != 21 { + t.Fatalf("unexpected cached hourly row: %#v", rows[0]) + } +} + +func TestReplaceVcenterAggregateTotalsFromSummary(t *testing.T) { + ctx := context.Background() + dbConn := newTestSQLiteDB(t) + + summaryTable := "inventory_daily_summary_20260101" + if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + "Vcenter" TEXT NOT NULL, + "Name" TEXT, + "VmId" TEXT, + "VmUuid" TEXT, + "AvgVcpuCount" REAL, + "VcpuCount" BIGINT, + "AvgRamGB" REAL, + "RamGB" BIGINT +)`, summaryTable)); err != nil { + t.Fatalf("failed to create summary table: %v", err) + } + insertSQL := fmt.Sprintf(` +INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") +VALUES (?,?,?,?,?,?) +`, summaryTable) + rows := [][]interface{}{ + {"vc-a", "vm-1", "1", "u1", 2.0, 4.0}, + {"vc-a", "vm-2", "2", "u2", 3.0, 5.0}, + {"vc-b", "vm-3", "3", "u3", 1.0, 2.0}, + } + for _, args := range rows { + if _, err := dbConn.ExecContext(ctx, insertSQL, args...); err != nil { + t.Fatalf("failed to insert summary row: %v", err) + } + } + + upserted, err := ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, summaryTable, "daily", 1_700_010_000) + if err != nil { + t.Fatalf("ReplaceVcenterAggregateTotalsFromSummary failed: %v", err) + } + if upserted != 2 { + t.Fatalf("expected 2 vcenter aggregate rows, got %d", upserted) + } + + vcA, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "daily", 10) + if err != nil { + t.Fatalf("ListVcenterAggregateTotals(vc-a) failed: %v", err) + } + if len(vcA) != 1 { + t.Fatalf("expected 1 vc-a daily row, got %d", len(vcA)) + } + if vcA[0].SnapshotTime != 1_700_010_000 || vcA[0].VmCount != 2 || vcA[0].VcpuTotal != 5 || vcA[0].RamTotalGB != 9 { + t.Fatalf("unexpected vc-a daily aggregate row: %#v", vcA[0]) + } + + vcB, err := ListVcenterAggregateTotalsSince(ctx, dbConn, "vc-b", "daily", time.Unix(1_700_009_000, 0)) + if err != nil { + t.Fatalf("ListVcenterAggregateTotalsSince(vc-b) failed: %v", err) + } + if len(vcB) != 1 || vcB[0].VmCount != 1 || vcB[0].VcpuTotal != 1 || vcB[0].RamTotalGB != 2 { + t.Fatalf("unexpected vc-b daily aggregate row: %#v", vcB) + } +} + +func TestListVcenterTotalsByTypeDailyFallbackWarmsCache(t *testing.T) { + ctx := context.Background() + dbConn := newTestSQLiteDB(t) + + if _, err := dbConn.ExecContext(ctx, ` +CREATE TABLE snapshot_registry ( + snapshot_type TEXT, + table_name TEXT, + snapshot_time BIGINT +)`); err != nil { + t.Fatalf("failed to create snapshot_registry: %v", err) + } + + summaryTable := "inventory_daily_summary_20260102" + if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + "Vcenter" TEXT NOT NULL, + "Name" TEXT, + "VmId" TEXT, + "VmUuid" TEXT, + "AvgVcpuCount" REAL, + "VcpuCount" BIGINT, + "AvgRamGB" REAL, + "RamGB" BIGINT +)`, summaryTable)); err != nil { + t.Fatalf("failed to create summary table: %v", err) + } + insertSQL := fmt.Sprintf(` +INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") +VALUES (?,?,?,?,?,?) +`, summaryTable) + for _, args := range [][]interface{}{ + {"vc-a", "vm-1", "1", "u1", 4.0, 8.0}, + {"vc-a", "vm-2", "2", "u2", 2.0, 6.0}, + } { + if _, err := dbConn.ExecContext(ctx, insertSQL, args...); err != nil { + t.Fatalf("failed to insert summary row: %v", err) + } + } + if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", summaryTable, int64(1_700_020_000)); err != nil { + t.Fatalf("failed to insert snapshot_registry row: %v", err) + } + + rows, err := ListVcenterTotalsByType(ctx, dbConn, "vc-a", "daily", 10) + if err != nil { + t.Fatalf("ListVcenterTotalsByType failed: %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 daily row, got %d", len(rows)) + } + if rows[0].SnapshotTime != 1_700_020_000 || rows[0].VmCount != 2 || rows[0].VcpuTotal != 6 || rows[0].RamTotalGB != 14 { + t.Fatalf("unexpected daily totals row: %#v", rows[0]) + } + + cached, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "daily", 10) + if err != nil { + t.Fatalf("ListVcenterAggregateTotals failed: %v", err) + } + if len(cached) != 1 || cached[0].SnapshotTime != 1_700_020_000 || cached[0].VmCount != 2 { + t.Fatalf("expected warmed daily cache row, got %#v", cached) + } +} + +func TestSyncVcenterAggregateTotalsFromRegistry(t *testing.T) { + ctx := context.Background() + dbConn := newTestSQLiteDB(t) + + if _, err := dbConn.ExecContext(ctx, ` +CREATE TABLE snapshot_registry ( + snapshot_type TEXT, + table_name TEXT, + snapshot_time BIGINT +)`); err != nil { + t.Fatalf("failed to create snapshot_registry: %v", err) + } + + table1 := "inventory_daily_summary_20260103" + table2 := "inventory_daily_summary_20260104" + for _, table := range []string{table1, table2} { + if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + "Vcenter" TEXT NOT NULL, + "Name" TEXT, + "VmId" TEXT, + "VmUuid" TEXT, + "AvgVcpuCount" REAL, + "VcpuCount" BIGINT, + "AvgRamGB" REAL, + "RamGB" BIGINT +)`, table)); err != nil { + t.Fatalf("failed to create summary table %s: %v", table, err) + } + } + insert1 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table1) + insert2 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table2) + for _, args := range [][]interface{}{ + {"vc-a", "vm-1", "1", "u1", 2.0, 4.0}, + {"vc-b", "vm-2", "2", "u2", 3.0, 5.0}, + } { + if _, err := dbConn.ExecContext(ctx, insert1, args...); err != nil { + t.Fatalf("failed to insert row into %s: %v", table1, err) + } + } + if _, err := dbConn.ExecContext(ctx, insert2, "vc-a", "vm-3", "3", "u3", 4.0, 6.0); err != nil { + t.Fatalf("failed to insert row into %s: %v", table2, err) + } + if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", table1, int64(1_700_030_000)); err != nil { + t.Fatalf("failed to insert snapshot_registry row for table1: %v", err) + } + if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", table2, int64(1_700_040_000)); err != nil { + t.Fatalf("failed to insert snapshot_registry row for table2: %v", err) + } + + snapshotsRefreshed, rowsUpserted, err := SyncVcenterAggregateTotalsFromRegistry(ctx, dbConn, "daily") + if err != nil { + t.Fatalf("SyncVcenterAggregateTotalsFromRegistry failed: %v", err) + } + if snapshotsRefreshed != 2 { + t.Fatalf("expected 2 snapshots refreshed, got %d", snapshotsRefreshed) + } + if rowsUpserted != 3 { + t.Fatalf("expected 3 rows upserted, got %d", rowsUpserted) + } + + rows, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "daily", 10) + if err != nil { + t.Fatalf("ListVcenterAggregateTotals failed: %v", err) + } + if len(rows) != 2 { + t.Fatalf("expected 2 daily rows for vc-a, got %d", len(rows)) + } + if rows[0].SnapshotTime != 1_700_040_000 || rows[0].VmCount != 1 || rows[0].VcpuTotal != 4 || rows[0].RamTotalGB != 6 { + t.Fatalf("unexpected latest vc-a daily row: %#v", rows[0]) + } + if rows[1].SnapshotTime != 1_700_030_000 || rows[1].VmCount != 1 || rows[1].VcpuTotal != 2 || rows[1].RamTotalGB != 4 { + t.Fatalf("unexpected older vc-a daily row: %#v", rows[1]) + } +} diff --git a/internal/tasks/dailyAggregate.go b/internal/tasks/dailyAggregate.go index 24bfc01..3069923 100644 --- a/internal/tasks/dailyAggregate.go +++ b/internal/tasks/dailyAggregate.go @@ -182,6 +182,11 @@ func (c *CronTask) aggregateDailySummary(ctx context.Context, targetTime time.Ti } else { c.Logger.Debug("Registered daily snapshot", "table", summaryTable, "duration", time.Since(registerStart)) } + if refreshed, err := db.ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, summaryTable, "daily", dayStart.Unix()); err != nil { + c.Logger.Warn("failed to refresh vcenter daily aggregate totals cache", "error", err, "table", summaryTable) + } else { + c.Logger.Debug("refreshed vcenter daily aggregate totals cache", "table", summaryTable, "rows", refreshed) + } reportStart := time.Now() c.Logger.Debug("Generating daily report", "table", summaryTable) @@ -432,6 +437,11 @@ LIMIT 1 } else { c.Logger.Debug("Registered daily snapshot", "table", summaryTable, "duration", time.Since(registerStart)) } + if refreshed, err := db.ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, summaryTable, "daily", dayStart.Unix()); err != nil { + c.Logger.Warn("failed to refresh vcenter daily aggregate totals cache", "error", err, "table", summaryTable) + } else { + c.Logger.Debug("refreshed vcenter daily aggregate totals cache", "table", summaryTable, "rows", refreshed) + } reportStart := time.Now() c.Logger.Debug("Generating daily report", "table", summaryTable) if err := c.generateReport(ctx, summaryTable); err != nil { diff --git a/main.go b/main.go index 9544ddb..88fc5c9 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "strings" "time" "vctp/db" + "vctp/internal/report" "vctp/internal/secrets" "vctp/internal/settings" "vctp/internal/tasks" @@ -42,6 +43,7 @@ func main() { settingsPath := flag.String("settings", "/etc/dtms/vctp.yml", "Path to settings YAML") runInventory := flag.Bool("run-inventory", false, "Run a single inventory snapshot across all configured vCenters and exit") dbCleanup := flag.Bool("db-cleanup", false, "Run a one-time cleanup to drop low-value hourly snapshot indexes and exit") + backfillVcenterCache := flag.Bool("backfill-vcenter-cache", false, "Run a one-time backfill for vcenter latest+aggregate cache tables and exit") flag.Parse() bootstrapLogger := log.New(log.LevelInfo, log.OutputText) @@ -104,6 +106,60 @@ func main() { logger.Info("completed hourly snapshot index cleanup", "indexes_dropped", dropped) return } + if *backfillVcenterCache { + logger.Info("starting one-time vcenter cache backfill") + if err := report.EnsureSnapshotRegistry(ctx, database); err != nil { + logger.Error("failed to ensure snapshot registry", "error", err) + os.Exit(1) + } + hourlyRecords, err := report.ListSnapshots(ctx, database, "hourly") + if err != nil { + logger.Error("failed to list hourly snapshots from registry", "error", err) + os.Exit(1) + } + if len(hourlyRecords) == 0 { + logger.Warn("snapshot registry has no hourly entries; attempting registry migration before cache backfill") + stats, err := report.MigrateSnapshotRegistry(ctx, database) + if err != nil { + logger.Error("failed to migrate snapshot registry before cache backfill", "error", err) + os.Exit(1) + } + logger.Info("snapshot registry migration complete", + "hourly_renamed", stats.HourlyRenamed, + "hourly_registered", stats.HourlyRegistered, + "daily_registered", stats.DailyRegistered, + "monthly_registered", stats.MonthlyRegistered, + ) + } + + if err := db.SyncVcenterTotalsFromSnapshots(ctx, database.DB()); err != nil { + logger.Error("failed to backfill hourly vcenter totals cache", "error", err) + os.Exit(1) + } + latestSynced, err := db.SyncVcenterLatestTotalsFromHistory(ctx, database.DB()) + if err != nil { + logger.Error("failed to backfill latest vcenter totals cache", "error", err) + os.Exit(1) + } + + dailySnapshots, dailyRows, dailyErr := db.SyncVcenterAggregateTotalsFromRegistry(ctx, database.DB(), "daily") + if dailyErr != nil { + logger.Warn("daily vcenter aggregate cache backfill completed with warnings", "error", dailyErr) + } + monthlySnapshots, monthlyRows, monthlyErr := db.SyncVcenterAggregateTotalsFromRegistry(ctx, database.DB(), "monthly") + if monthlyErr != nil { + logger.Warn("monthly vcenter aggregate cache backfill completed with warnings", "error", monthlyErr) + } + + logger.Info("completed one-time vcenter cache backfill", + "latest_rows_synced", latestSynced, + "daily_snapshots_refreshed", dailySnapshots, + "daily_rows_upserted", dailyRows, + "monthly_snapshots_refreshed", monthlySnapshots, + "monthly_rows_upserted", monthlyRows, + ) + return + } // Determine bind IP bindIP := strings.TrimSpace(s.Values.Settings.BindIP) diff --git a/server/handler/vcenters.go b/server/handler/vcenters.go index c48f01f..85bbe17 100644 --- a/server/handler/vcenters.go +++ b/server/handler/vcenters.go @@ -1,6 +1,7 @@ package handler import ( + "context" "fmt" "net/http" "net/url" @@ -11,6 +12,12 @@ import ( "vctp/db" ) +const ( + vcenterHourlyDetailWindowDays = 45 + vcenterDailyDefaultLimit = 400 + vcenterMonthlyDefaultLimit = 200 +) + // VcenterList renders a list of vCenters being monitored. // @Summary List vCenters // @Description Lists all vCenters with recorded snapshot totals. @@ -20,9 +27,6 @@ import ( // @Router /vcenters [get] func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil { - h.Logger.Warn("failed to sync vcenter totals", "error", err) - } vcs, err := db.ListVcenters(ctx, h.Database.DB()) if err != nil { http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError) @@ -32,7 +36,7 @@ func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) { for _, vc := range vcs { links = append(links, views.VcenterLink{ Name: vc, - Link: "/vcenters/totals?vcenter=" + url.QueryEscape(vc), + Link: "/vcenters/totals/daily?vcenter=" + url.QueryEscape(vc), }) } w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -41,49 +45,117 @@ func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) { } } -// VcenterTotals renders totals for a vCenter. +// VcenterTotals keeps backward compatibility with the original endpoint and routes to the new pages. // @Summary vCenter totals -// @Description Shows per-snapshot totals for a vCenter. +// @Description Redirect-style handler for compatibility; use /vcenters/totals/daily or /vcenters/totals/hourly. // @Tags vcenters // @Produce text/html // @Param vcenter query string true "vCenter URL" -// @Param type query string false "hourly|daily|monthly (default: hourly)" -// @Param limit query int false "Limit results (default 200)" +// @Param type query string false "hourly|daily|monthly" +// @Param limit query int false "Limit results" // @Success 200 {string} string "HTML page" // @Failure 400 {string} string "Missing vcenter" // @Router /vcenters/totals [get] func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) { + switch strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type"))) { + case "hourly", "hourly-detail", "detail", "detailed": + h.VcenterTotalsHourlyDetailed(w, r) + return + case "monthly": + h.vcenterTotalsLegacyMonthly(w, r) + return + default: + h.VcenterTotalsDaily(w, r) + return + } +} + +// VcenterTotalsDaily renders the daily-aggregation totals page for one vCenter. +// @Summary vCenter daily totals +// @Description Shows daily aggregated VM count/vCPU/RAM totals for a vCenter. +// @Tags vcenters +// @Produce text/html +// @Param vcenter query string true "vCenter URL" +// @Param limit query int false "Limit results (default 400)" +// @Success 200 {string} string "HTML page" +// @Failure 400 {string} string "Missing vcenter" +// @Router /vcenters/totals/daily [get] +func (h *Handler) VcenterTotalsDaily(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - vc := r.URL.Query().Get("vcenter") + vc, ok := requiredVcenterParam(w, r) + if !ok { + return + } + limit := parsePositiveLimit(r, vcenterDailyDefaultLimit) + rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, "daily", limit) + if err != nil { + http.Error(w, fmt.Sprintf("failed to list daily totals: %v", err), http.StatusInternalServerError) + return + } + h.renderVcenterTotalsPage(ctx, w, vc, "daily", rows) +} + +// VcenterTotalsHourlyDetailed renders a detailed hourly page over the most recent 45 days. +// @Summary vCenter hourly totals (45 days) +// @Description Shows detailed hourly VM count/vCPU/RAM totals for the latest 45 days. +// @Tags vcenters +// @Produce text/html +// @Param vcenter query string true "vCenter URL" +// @Success 200 {string} string "HTML page" +// @Failure 400 {string} string "Missing vcenter" +// @Router /vcenters/totals/hourly [get] +func (h *Handler) VcenterTotalsHourlyDetailed(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vc, ok := requiredVcenterParam(w, r) + if !ok { + return + } + since := time.Now().AddDate(0, 0, -vcenterHourlyDetailWindowDays) + rows, err := db.ListVcenterHourlyTotalsSince(ctx, h.Database.DB(), vc, since) + if err != nil { + http.Error(w, fmt.Sprintf("failed to list hourly totals: %v", err), http.StatusInternalServerError) + return + } + h.renderVcenterTotalsPage(ctx, w, vc, "hourly45", rows) +} + +func (h *Handler) vcenterTotalsLegacyMonthly(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vc, ok := requiredVcenterParam(w, r) + if !ok { + return + } + limit := parsePositiveLimit(r, vcenterMonthlyDefaultLimit) + rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, "monthly", limit) + if err != nil { + http.Error(w, fmt.Sprintf("failed to list monthly totals: %v", err), http.StatusInternalServerError) + return + } + h.renderVcenterTotalsPage(ctx, w, vc, "monthly", rows) +} + +func requiredVcenterParam(w http.ResponseWriter, r *http.Request) (string, bool) { + vc := strings.TrimSpace(r.URL.Query().Get("vcenter")) if vc == "" { http.Error(w, "vcenter is required", http.StatusBadRequest) - return + return "", false } - viewType := strings.ToLower(r.URL.Query().Get("type")) - if viewType == "" { - viewType = "hourly" + return vc, true +} + +func parsePositiveLimit(r *http.Request, defaultLimit int) int { + if defaultLimit <= 0 { + defaultLimit = 200 } - switch viewType { - case "hourly", "daily", "monthly": - default: - viewType = "hourly" - } - if viewType == "hourly" { - if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil { - h.Logger.Warn("failed to sync vcenter totals", "error", err) + if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" { + if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 { + return parsed } } - limit := 200 - if l := r.URL.Query().Get("limit"); l != "" { - if v, err := strconv.Atoi(l); err == nil && v > 0 { - limit = v - } - } - rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, viewType, limit) - if err != nil { - http.Error(w, fmt.Sprintf("failed to list totals: %v", err), http.StatusInternalServerError) - return - } + return defaultLimit +} + +func (h *Handler) renderVcenterTotalsPage(ctx context.Context, w http.ResponseWriter, vc string, viewType string, rows []db.VcenterTotalRow) { entries := make([]views.VcenterTotalsEntry, 0, len(rows)) for _, row := range rows { entries = append(entries, views.VcenterTotalsEntry{ @@ -105,28 +177,26 @@ func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) { func buildVcenterMeta(vcenter string, viewType string) views.VcenterTotalsMeta { active := viewType if active == "" { - active = "hourly" + active = "daily" } meta := views.VcenterTotalsMeta{ - ViewType: active, - TypeLabel: "Hourly", - HourlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=hourly", - DailyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=daily", - MonthlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=monthly", - HourlyClass: "web3-button", - DailyClass: "web3-button", - MonthlyClass: "web3-button", + ViewType: active, + TypeLabel: "Daily", + HourlyLink: "/vcenters/totals/hourly?vcenter=" + url.QueryEscape(vcenter), + DailyLink: "/vcenters/totals/daily?vcenter=" + url.QueryEscape(vcenter), + HourlyClass: "web3-button", + DailyClass: "web3-button", } switch active { - case "daily": - meta.TypeLabel = "Daily" - meta.DailyClass = "web3-button active" + case "hourly45", "hourly": + meta.ViewType = "hourly45" + meta.TypeLabel = fmt.Sprintf("Hourly (last %d days)", vcenterHourlyDetailWindowDays) + meta.HourlyClass = "web3-button active" case "monthly": meta.TypeLabel = "Monthly" - meta.MonthlyClass = "web3-button active" default: - meta.ViewType = "hourly" - meta.HourlyClass = "web3-button active" + meta.ViewType = "daily" + meta.DailyClass = "web3-button active" } return meta } diff --git a/server/router/router.go b/server/router/router.go index fa46728..398efd1 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -72,6 +72,8 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st mux.HandleFunc("/vm/trace", h.VmTrace) mux.HandleFunc("/vcenters", h.VcenterList) mux.HandleFunc("/vcenters/totals", h.VcenterTotals) + mux.HandleFunc("/vcenters/totals/daily", h.VcenterTotalsDaily) + mux.HandleFunc("/vcenters/totals/hourly", h.VcenterTotalsHourlyDetailed) mux.HandleFunc("/metrics", h.Metrics) mux.HandleFunc("/snapshots/hourly", h.SnapshotHourlyList) diff --git a/vctp.yml b/vctp-service.yml similarity index 94% rename from vctp.yml rename to vctp-service.yml index abe6a53..7ef1fa9 100644 --- a/vctp.yml +++ b/vctp-service.yml @@ -1,7 +1,7 @@ name: "vctp" arch: "amd64" platform: "linux" -version: "v26.1.2" +version: "v26.1.3" version_schema: semver description: vCTP monitors VMware VM inventory and event data to build chargeback reports maintainer: "@coadn" @@ -13,7 +13,7 @@ contents: - src: build/vctp-linux-amd64 dst: /usr/bin/vctp-linux-amd64 - - src: src/vctp.yml + - src: src/vctp-service.yml dst: /etc/dtms/vctp.yml type: config|noreplace file_info: