Files
vctp2/server/handler/vcenters.go
Nathan Coad 871904f63e
All checks were successful
continuous-integration/drone/push Build is passing
add vcenter totals line graph
2026-01-16 12:36:53 +11:00

226 lines
6.6 KiB
Go

package handler
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"vctp/components/views"
"vctp/db"
)
// VcenterList renders a list of vCenters being monitored.
// @Summary List vCenters
// @Description Lists all vCenters with recorded snapshot totals.
// @Tags vcenters
// @Produce text/html
// @Success 200 {string} string "HTML page"
// @Router /vcenters [get]
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_ = db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB())
vcs, err := db.ListVcenters(ctx, h.Database.DB())
if err != nil {
http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError)
return
}
links := make([]views.VcenterLink, 0, len(vcs))
for _, vc := range vcs {
links = append(links, views.VcenterLink{
Name: vc,
Link: "/vcenters/totals?vcenter=" + url.QueryEscape(vc),
})
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := views.VcenterList(links).Render(ctx, w); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
}
// VcenterTotals renders totals for a vCenter.
// @Summary vCenter totals
// @Description Shows per-snapshot totals for a vCenter.
// @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)"
// @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) {
ctx := r.Context()
vc := r.URL.Query().Get("vcenter")
if vc == "" {
http.Error(w, "vcenter is required", http.StatusBadRequest)
return
}
viewType := strings.ToLower(r.URL.Query().Get("type"))
if viewType == "" {
viewType = "hourly"
}
switch viewType {
case "hourly", "daily", "monthly":
default:
viewType = "hourly"
}
if viewType == "hourly" {
_ = db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB())
}
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
}
entries := make([]views.VcenterTotalsEntry, 0, len(rows))
for _, row := range rows {
entries = append(entries, views.VcenterTotalsEntry{
Snapshot: time.Unix(row.SnapshotTime, 0).Local().Format("2006-01-02 15:04:05"),
RawTime: row.SnapshotTime,
VmCount: row.VmCount,
VcpuTotal: row.VcpuTotal,
RamTotalGB: row.RamTotalGB,
})
}
chart := buildVcenterChart(entries)
meta := buildVcenterMeta(vc, viewType)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := views.VcenterTotalsPage(vc, entries, chart, meta).Render(ctx, w); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
}
func buildVcenterMeta(vcenter string, viewType string) views.VcenterTotalsMeta {
active := viewType
if active == "" {
active = "hourly"
}
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",
}
switch active {
case "daily":
meta.TypeLabel = "Daily"
meta.DailyClass = "web3-button active"
case "monthly":
meta.TypeLabel = "Monthly"
meta.MonthlyClass = "web3-button active"
default:
meta.ViewType = "hourly"
meta.HourlyClass = "web3-button active"
}
return meta
}
func buildVcenterChart(entries []views.VcenterTotalsEntry) views.VcenterChartData {
if len(entries) == 0 {
return views.VcenterChartData{}
}
// Plot oldest on the left, newest on the right.
plot := make([]views.VcenterTotalsEntry, 0, len(entries))
for i := len(entries) - 1; i >= 0; i-- {
plot = append(plot, entries[i])
}
width := 1200.0
height := 260.0
maxVal := float64(0)
for _, e := range plot {
if float64(e.VmCount) > maxVal {
maxVal = float64(e.VmCount)
}
if float64(e.VcpuTotal) > maxVal {
maxVal = float64(e.VcpuTotal)
}
if float64(e.RamTotalGB) > maxVal {
maxVal = float64(e.RamTotalGB)
}
}
if maxVal == 0 {
maxVal = 1
}
stepX := width
if len(plot) > 1 {
stepX = width / float64(len(plot)-1)
}
pointsVm := ""
pointsVcpu := ""
pointsRam := ""
for i, e := range plot {
x := 40 + float64(i)*stepX
yVm := 10 + (1-(float64(e.VmCount)/maxVal))*height
yVcpu := 10 + (1-(float64(e.VcpuTotal)/maxVal))*height
yRam := 10 + (1-(float64(e.RamTotalGB)/maxVal))*height
if i == 0 {
pointsVm = fmt.Sprintf("%.1f,%.1f", x, yVm)
pointsVcpu = fmt.Sprintf("%.1f,%.1f", x, yVcpu)
pointsRam = fmt.Sprintf("%.1f,%.1f", x, yRam)
} else {
pointsVm = pointsVm + " " + fmt.Sprintf("%.1f,%.1f", x, yVm)
pointsVcpu = pointsVcpu + " " + fmt.Sprintf("%.1f,%.1f", x, yVcpu)
pointsRam = pointsRam + " " + fmt.Sprintf("%.1f,%.1f", x, yRam)
}
}
gridX := []float64{}
if len(plot) > 1 {
for i := 0; i < len(plot); i++ {
gridX = append(gridX, 40+float64(i)*stepX)
}
}
gridY := []float64{}
for i := 0; i <= 4; i++ {
gridY = append(gridY, 10+float64(i)*(height/4))
}
yTicks := []views.ChartTick{}
for i := 0; i <= 4; i++ {
val := maxVal * float64(4-i) / 4
pos := 10 + float64(i)*(height/4)
yTicks = append(yTicks, views.ChartTick{Pos: pos, Label: fmt.Sprintf("%.0f", val)})
}
xTicks := []views.ChartTick{}
maxTicks := 6
stepIdx := 1
if len(plot) > 1 {
stepIdx = (len(plot)-1)/maxTicks + 1
}
for idx := 0; idx < len(plot); idx += stepIdx {
x := 40 + float64(idx)*stepX
label := time.Unix(plot[idx].RawTime, 0).Local().Format("01-02 15:04")
xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label})
}
if len(plot) > 1 {
lastIdx := len(plot) - 1
if (lastIdx % stepIdx) != 0 {
x := 40 + float64(lastIdx)*stepX
label := time.Unix(plot[lastIdx].RawTime, 0).Local().Format("01-02 15:04")
xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label})
}
}
return views.VcenterChartData{
PointsVm: pointsVm,
PointsVcpu: pointsVcpu,
PointsRam: pointsRam,
Width: int(width),
Height: int(height),
GridX: gridX,
GridY: gridY,
YTicks: yTicks,
XTicks: xTicks,
}
}