From 3e2d95d3b934d8d222f2772fc022f1a566c053e0 Mon Sep 17 00:00:00 2001
From: Nathan Coad
Date: Fri, 23 Jan 2026 09:38:08 +1100
Subject: [PATCH] fix aggregation logic
---
README.md | 2 +-
components/core/.gitignore | 1 +
components/core/header_templ.go | 2 +-
components/views/.gitignore | 1 +
components/views/index.templ | 2 +-
components/views/index_templ.go | 8 +-
components/views/snapshots_templ.go | 100 ++++++++--------
components/views/vm_trace_templ.go | 88 +++++++-------
internal/report/snapshots.go | 2 +
internal/tasks/dailyAggregate.go | 15 +--
internal/tasks/inventoryHelpers.go | 41 +++++--
internal/tasks/inventoryLifecycle.go | 4 +
internal/tasks/inventorySnapshots.go | 89 +++++++++++----
internal/tasks/monthlyAggregate.go | 164 +++++++++++++++++++++++----
internal/vcenter/vcenter.go | 27 +++--
server/handler/snapshots.go | 6 +-
16 files changed, 384 insertions(+), 168 deletions(-)
create mode 100644 components/core/.gitignore
create mode 100644 components/views/.gitignore
diff --git a/README.md b/README.md
index 101c6ad..0506223 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Daily aggregation runs per VM using sample counts for the day:
- `AvgIsPresent`: `SamplesPresent / TotalSamples` (0 when `TotalSamples` is 0).
- `AvgVcpuCount`, `AvgRamGB`, `AvgProvisionedDisk` (daily): `sum(values_per_sample) / TotalSamples` to time‑weight config changes and prorate partial‑day VMs.
- `PoolTinPct`, `PoolBronzePct`, `PoolSilverPct`, `PoolGoldPct` (daily): `(pool_hits / SamplesPresent) * 100`, so pool percentages reflect only the time the VM existed.
-- `CreationTime`: only set when vCenter provides it or when a lifecycle cache `FirstSeen` is available; otherwise it remains `0`.
+- `CreationTime`: only set when vCenter provides it; otherwise it remains `0`.
Monthly aggregation builds on daily summaries (or the daily rollup cache):
- For each VM, daily averages are converted to weighted sums: `daily_avg * daily_total_samples`.
diff --git a/components/core/.gitignore b/components/core/.gitignore
new file mode 100644
index 0000000..472fecd
--- /dev/null
+++ b/components/core/.gitignore
@@ -0,0 +1 @@
+*.go
\ No newline at end of file
diff --git a/components/core/header_templ.go b/components/core/header_templ.go
index 3849028..0d57eba 100644
--- a/components/core/header_templ.go
+++ b/components/core/header_templ.go
@@ -38,7 +38,7 @@ func Header() templ.Component {
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs("/assets/css/output@" + version.Value + ".css")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/core/header.templ`, Line: 15, Col: 61}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/header.templ`, Line: 15, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
diff --git a/components/views/.gitignore b/components/views/.gitignore
new file mode 100644
index 0000000..472fecd
--- /dev/null
+++ b/components/views/.gitignore
@@ -0,0 +1 @@
+*.go
\ No newline at end of file
diff --git a/components/views/index.templ b/components/views/index.templ
index 2bb75fc..56bafe2 100644
--- a/components/views/index.templ
+++ b/components/views/index.templ
@@ -73,7 +73,7 @@ templ Index(info BuildInfo) {
Daily AvgVcpuCount/AvgRamGB/AvgProvisionedDisk = sum of per-sample values divided by TotalSamples (time-weighted).
Daily pool percentages use pool hits divided by SamplesPresent, so they reflect only the time the VM existed.
Monthly aggregation weights daily averages by daily total samples, then divides by monthly total samples.
- CreationTime is only set when vCenter provides it or when lifecycle FirstSeen is available; otherwise it remains 0.
+ CreationTime is only set when vCenter provides it; otherwise it remains 0.
diff --git a/components/views/index_templ.go b/components/views/index_templ.go
index d9e69e0..9eae4b0 100644
--- a/components/views/index_templ.go
+++ b/components/views/index_templ.go
@@ -54,7 +54,7 @@ func Index(info BuildInfo) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(info.BuildTime)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/index.templ`, Line: 40, Col: 59}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 40, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -67,7 +67,7 @@ func Index(info BuildInfo) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(info.SHA1Ver)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/index.templ`, Line: 44, Col: 57}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 44, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -80,13 +80,13 @@ func Index(info BuildInfo) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(info.GoVersion)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/index.templ`, Line: 48, Col: 59}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 48, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
Overview
vCTP is a vSphere Chargeback Tracking Platform.
Snapshots and Reports
- Hourly snapshots capture inventory per vCenter (concurrency via `hourly_snapshot_concurrency`).
- Daily summaries aggregate the hourly snapshots for the day; monthly summaries aggregate daily summaries for the month (or hourly snapshots if configured).
- Snapshots are registered in `snapshot_registry` so regeneration via `/api/snapshots/aggregate` can locate the correct tables (fallback scanning is also supported).
- Reports (XLSX with totals/charts) are generated automatically after hourly, daily, and monthly jobs and written to a reports directory.
Prorating and Aggregation
- SamplesPresent is the count of snapshots in which the VM appears; TotalSamples is the count of unique snapshot times for the vCenter.
- AvgIsPresent = SamplesPresent / TotalSamples (0 when TotalSamples is 0).
- Daily AvgVcpuCount/AvgRamGB/AvgProvisionedDisk = sum of per-sample values divided by TotalSamples (time-weighted).
- Daily pool percentages use pool hits divided by SamplesPresent, so they reflect only the time the VM existed.
- Monthly aggregation weights daily averages by daily total samples, then divides by monthly total samples.
- CreationTime is only set when vCenter provides it or when lifecycle FirstSeen is available; otherwise it remains 0.