add charts
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2026-01-14 11:08:01 +11:00
parent 13af853c45
commit 5c34a9eacd
2 changed files with 454 additions and 10 deletions

View File

@@ -420,7 +420,11 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
return nil, fmt.Errorf("no columns found for table %s", tableName)
}
humanizeTimes := strings.HasPrefix(tableName, "inventory_daily_summary_") || strings.HasPrefix(tableName, "inventory_monthly_summary_")
isHourlySnapshot := strings.HasPrefix(tableName, "inventory_hourly_")
isDailySummary := strings.HasPrefix(tableName, "inventory_daily_summary_")
isMonthlySummary := strings.HasPrefix(tableName, "inventory_monthly_summary_")
hideInventoryID := isHourlySnapshot || isDailySummary || isMonthlySummary
humanizeTimes := isDailySummary || isMonthlySummary
type columnSpec struct {
Name string
SourceIndex int
@@ -428,6 +432,9 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
}
specs := make([]columnSpec, 0, len(columns)+2)
for i, columnName := range columns {
if hideInventoryID && strings.EqualFold(columnName, "InventoryId") {
continue
}
specs = append(specs, columnSpec{Name: columnName, SourceIndex: i})
if humanizeTimes && columnName == "CreationTime" {
specs = append(specs, columnSpec{Name: "CreationTimeReadable", SourceIndex: i, Humanize: true})
@@ -521,12 +528,59 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
logger.Error("Error freezing top row", "error", err)
}
addTotalsChartSheet(logger, Database, ctx, xlsx, tableName)
if err := xlsx.Write(&buffer); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func addTotalsChartSheet(logger *slog.Logger, database db.Database, ctx context.Context, xlsx *excelize.File, tableName string) {
if strings.HasPrefix(tableName, "inventory_daily_summary_") {
suffix := strings.TrimPrefix(tableName, "inventory_daily_summary_")
dayStart, err := time.Parse("20060102", suffix)
if err != nil {
return
}
dayEnd := dayStart.AddDate(0, 0, 1)
if err := EnsureSnapshotRegistry(ctx, database); err != nil {
return
}
records, err := ListSnapshotsByRange(ctx, database, "hourly", dayStart, dayEnd)
if err != nil || len(records) == 0 {
return
}
points, err := buildHourlyTotals(ctx, database.DB(), records)
if err != nil || len(points) == 0 {
return
}
writeTotalsChart(logger, xlsx, "Hourly Totals", points)
return
}
if strings.HasPrefix(tableName, "inventory_monthly_summary_") {
suffix := strings.TrimPrefix(tableName, "inventory_monthly_summary_")
monthStart, err := time.Parse("200601", suffix)
if err != nil {
return
}
monthEnd := monthStart.AddDate(0, 1, 0)
if err := EnsureSnapshotRegistry(ctx, database); err != nil {
return
}
records, err := ListSnapshotsByRange(ctx, database, "daily", monthStart, monthEnd)
if err != nil || len(records) == 0 {
return
}
points, err := buildDailyTotals(ctx, database.DB(), records)
if err != nil || len(points) == 0 {
return
}
writeTotalsChart(logger, xlsx, "Daily Totals", points)
}
}
func validateTableName(name string) error {
if name == "" {
return fmt.Errorf("table name is empty")
@@ -639,6 +693,163 @@ func normalizeCellValue(value interface{}) interface{} {
}
}
type totalsPoint struct {
Label string
VmCount int64
VcpuTotal float64
RamTotal float64
PresenceRatio float64
TinTotal float64
BronzeTotal float64
SilverTotal float64
GoldTotal float64
}
func buildHourlyTotals(ctx context.Context, dbConn *sqlx.DB, records []SnapshotRecord) ([]totalsPoint, error) {
points := make([]totalsPoint, 0, len(records))
for _, record := range records {
if err := validateTableName(record.TableName); err != nil {
return nil, err
}
query := fmt.Sprintf(`
SELECT
COUNT(DISTINCT "VmId") AS vm_count,
COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND "VcpuCount" IS NOT NULL THEN "VcpuCount" ELSE 0 END), 0) AS vcpu_total,
COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND "RamGB" IS NOT NULL THEN "RamGB" ELSE 0 END), 0) AS ram_total,
COALESCE(AVG(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS presence_ratio,
COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'tin' THEN 1 ELSE 0 END), 0) AS tin_total,
COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'bronze' THEN 1 ELSE 0 END), 0) AS bronze_total,
COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'silver' THEN 1 ELSE 0 END), 0) AS silver_total,
COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'gold' THEN 1 ELSE 0 END), 0) AS gold_total
FROM %s
`, record.TableName)
var row struct {
VmCount int64 `db:"vm_count"`
VcpuTotal int64 `db:"vcpu_total"`
RamTotal int64 `db:"ram_total"`
PresenceRatio float64 `db:"presence_ratio"`
TinTotal float64 `db:"tin_total"`
BronzeTotal float64 `db:"bronze_total"`
SilverTotal float64 `db:"silver_total"`
GoldTotal float64 `db:"gold_total"`
}
if err := dbConn.GetContext(ctx, &row, query); err != nil {
return nil, err
}
points = append(points, totalsPoint{
Label: record.SnapshotTime.Local().Format("2006-01-02 15:04"),
VmCount: row.VmCount,
VcpuTotal: float64(row.VcpuTotal),
RamTotal: float64(row.RamTotal),
PresenceRatio: row.PresenceRatio,
TinTotal: row.TinTotal,
BronzeTotal: row.BronzeTotal,
SilverTotal: row.SilverTotal,
GoldTotal: row.GoldTotal,
})
}
return points, nil
}
func buildDailyTotals(ctx context.Context, dbConn *sqlx.DB, records []SnapshotRecord) ([]totalsPoint, error) {
points := make([]totalsPoint, 0, len(records))
for _, record := range records {
if err := validateTableName(record.TableName); err != nil {
return nil, err
}
query := fmt.Sprintf(`
SELECT
COUNT(DISTINCT "VmId") AS vm_count,
COALESCE(SUM(CASE WHEN "AvgVcpuCount" IS NOT NULL THEN "AvgVcpuCount" ELSE 0 END), 0) AS vcpu_total,
COALESCE(SUM(CASE WHEN "AvgRamGB" IS NOT NULL THEN "AvgRamGB" ELSE 0 END), 0) AS ram_total,
COALESCE(AVG(CASE WHEN "AvgIsPresent" IS NOT NULL THEN "AvgIsPresent" ELSE 0 END), 0) AS presence_ratio,
COALESCE(SUM(CASE WHEN "Tin" IS NOT NULL THEN "Tin" ELSE 0 END) / 100.0, 0) AS tin_total,
COALESCE(SUM(CASE WHEN "Bronze" IS NOT NULL THEN "Bronze" ELSE 0 END) / 100.0, 0) AS bronze_total,
COALESCE(SUM(CASE WHEN "Silver" IS NOT NULL THEN "Silver" ELSE 0 END) / 100.0, 0) AS silver_total,
COALESCE(SUM(CASE WHEN "Gold" IS NOT NULL THEN "Gold" ELSE 0 END) / 100.0, 0) AS gold_total
FROM %s
`, record.TableName)
var row struct {
VmCount int64 `db:"vm_count"`
VcpuTotal float64 `db:"vcpu_total"`
RamTotal float64 `db:"ram_total"`
PresenceRatio float64 `db:"presence_ratio"`
TinTotal float64 `db:"tin_total"`
BronzeTotal float64 `db:"bronze_total"`
SilverTotal float64 `db:"silver_total"`
GoldTotal float64 `db:"gold_total"`
}
if err := dbConn.GetContext(ctx, &row, query); err != nil {
return nil, err
}
points = append(points, totalsPoint{
Label: record.SnapshotTime.Local().Format("2006-01-02"),
VmCount: row.VmCount,
VcpuTotal: row.VcpuTotal,
RamTotal: row.RamTotal,
PresenceRatio: row.PresenceRatio,
TinTotal: row.TinTotal,
BronzeTotal: row.BronzeTotal,
SilverTotal: row.SilverTotal,
GoldTotal: row.GoldTotal,
})
}
return points, nil
}
func writeTotalsChart(logger *slog.Logger, xlsx *excelize.File, sheetName string, points []totalsPoint) {
if len(points) == 0 {
return
}
index := xlsx.NewSheet(sheetName)
xlsx.SetActiveSheet(index)
headers := []string{"Label", "VmCount", "VcpuCount", "RamGB", "PresenceRatio", "Tin", "Bronze", "Silver", "Gold"}
for i, header := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
xlsx.SetCellValue(sheetName, cell, header)
}
for i, point := range points {
row := i + 2
xlsx.SetCellValue(sheetName, fmt.Sprintf("A%d", row), point.Label)
xlsx.SetCellValue(sheetName, fmt.Sprintf("B%d", row), point.VmCount)
xlsx.SetCellValue(sheetName, fmt.Sprintf("C%d", row), point.VcpuTotal)
xlsx.SetCellValue(sheetName, fmt.Sprintf("D%d", row), point.RamTotal)
xlsx.SetCellValue(sheetName, fmt.Sprintf("E%d", row), point.PresenceRatio)
xlsx.SetCellValue(sheetName, fmt.Sprintf("F%d", row), point.TinTotal)
xlsx.SetCellValue(sheetName, fmt.Sprintf("G%d", row), point.BronzeTotal)
xlsx.SetCellValue(sheetName, fmt.Sprintf("H%d", row), point.SilverTotal)
xlsx.SetCellValue(sheetName, fmt.Sprintf("I%d", row), point.GoldTotal)
}
lastRow := len(points) + 1
chart := fmt.Sprintf(`{
"type": "line",
"series": [
{"name": "%s!$B$1", "categories": "%s!$A$2:$A$%d", "values": "%s!$B$2:$B$%d"},
{"name": "%s!$C$1", "categories": "%s!$A$2:$A$%d", "values": "%s!$C$2:$C$%d"},
{"name": "%s!$D$1", "categories": "%s!$A$2:$A$%d", "values": "%s!$D$2:$D$%d"},
{"name": "%s!$E$1", "categories": "%s!$A$2:$A$%d", "values": "%s!$E$2:$E$%d"},
{"name": "%s!$F$1", "categories": "%s!$A$2:$A$%d", "values": "%s!$F$2:$F$%d"},
{"name": "%s!$G$1", "categories": "%s!$A$2:$A$%d", "values": "%s!$G$2:$G$%d"},
{"name": "%s!$H$1", "categories": "%s!$A$2:$A$%d", "values": "%s!$H$2:$H$%d"},
{"name": "%s!$I$1", "categories": "%s!$A$2:$A$%d", "values": "%s!$I$2:$I$%d"}
],
"legend": {"position": "bottom"}
}`, sheetName, sheetName, lastRow, sheetName, lastRow,
sheetName, sheetName, lastRow, sheetName, lastRow,
sheetName, sheetName, lastRow, sheetName, lastRow,
sheetName, sheetName, lastRow, sheetName, lastRow,
sheetName, sheetName, lastRow, sheetName, lastRow,
sheetName, sheetName, lastRow, sheetName, lastRow,
sheetName, sheetName, lastRow, sheetName, lastRow,
sheetName, sheetName, lastRow, sheetName, lastRow)
if err := xlsx.AddChart(sheetName, "G2", chart); err != nil {
logger.Error("Error adding totals chart", "error", err)
}
}
func formatEpochHuman(value interface{}) string {
var epoch int64
switch v := value.(type) {