Files
vctp2/server/handler/vcenters.go
Nathan Coad a993aedf79
All checks were successful
continuous-integration/drone/push Build is passing
use javascript chart instead of svg
2026-02-06 16:42:48 +11:00

195 lines
5.7 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()
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)
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" {
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
h.Logger.Warn("failed to sync vcenter totals", "error", err)
}
}
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])
}
labels := make([]string, 0, len(plot))
tickLabels := make([]string, 0, len(plot))
vmValues := make([]float64, 0, len(plot))
vcpuValues := make([]float64, 0, len(plot))
ramValues := make([]float64, 0, len(plot))
for _, e := range plot {
t := time.Unix(e.RawTime, 0).Local()
labels = append(labels, t.Format("2006-01-02 15:04:05"))
tickLabels = append(tickLabels, t.Format("01-02 15:04"))
vmValues = append(vmValues, float64(e.VmCount))
vcpuValues = append(vcpuValues, float64(e.VcpuTotal))
ramValues = append(ramValues, float64(e.RamTotalGB))
}
cfg := lineChartConfig{
Height: 360,
XTicks: 6,
YTicks: 5,
YLabel: "Totals",
XLabel: "Snapshots (oldest left, newest right)",
Labels: labels,
TickLabels: tickLabels,
Series: []lineChartSeries{
{
Name: "VMs",
Color: "#2563eb",
Values: vmValues,
TooltipFormat: "int",
LineWidth: 2.5,
},
{
Name: "vCPU",
Color: "#16a34a",
Values: vcpuValues,
TooltipFormat: "int",
LineWidth: 2.5,
},
{
Name: "RAM (GB)",
Color: "#ea580c",
Values: ramValues,
TooltipFormat: "int",
LineWidth: 2.5,
},
},
}
return views.VcenterChartData{
ConfigJSON: encodeLineChartConfig(cfg),
}
}