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]) } width := 1200.0 height := 260.0 plotWidth := width - 60.0 startX := 40.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 := plotWidth if len(plot) > 1 { stepX = plotWidth / float64(len(plot)-1) } pointsVm := "" pointsVcpu := "" pointsRam := "" for i, e := range plot { x := startX + 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, startX+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 := startX + 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 xLast := startX + float64(lastIdx)*stepX labelLast := time.Unix(plot[lastIdx].RawTime, 0).Local().Format("01-02 15:04") if len(xTicks) == 0 || xTicks[len(xTicks)-1].Pos != xLast { xTicks = append(xTicks, views.ChartTick{Pos: xLast, Label: labelLast}) } } return views.VcenterChartData{ PointsVm: pointsVm, PointsVcpu: pointsVcpu, PointsRam: pointsRam, Width: int(width), Height: int(height), GridX: gridX, GridY: gridY, YTicks: yTicks, XTicks: xTicks, } }