add vcenter totals line graph
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -13,124 +13,6 @@ templ Header() {
|
|||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/>
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/>
|
||||||
<script src="/assets/js/htmx@v2.0.2.min.js"></script>
|
<script src="/assets/js/htmx@v2.0.2.min.js"></script>
|
||||||
<link href={ "/assets/css/output@" + version.Value + ".css" } rel="stylesheet"/>
|
<link href={ "/assets/css/output@" + version.Value + ".css" } rel="stylesheet"/>
|
||||||
<style>
|
<link href="/assets/css/web3.css" rel="stylesheet"/>
|
||||||
:root {
|
|
||||||
--web2-blue: #1d9bf0;
|
|
||||||
--web2-slate: #0f172a;
|
|
||||||
--web2-muted: #64748b;
|
|
||||||
--web2-card: #ffffff;
|
|
||||||
--web2-border: #e5e7eb;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
|
||||||
color: var(--web2-slate);
|
|
||||||
}
|
|
||||||
.web2-bg {
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
.web2-shell {
|
|
||||||
max-width: 1100px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem 1.5rem 4rem;
|
|
||||||
}
|
|
||||||
.web2-header {
|
|
||||||
background: var(--web2-card);
|
|
||||||
border: 1px solid var(--web2-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
}
|
|
||||||
.web2-card {
|
|
||||||
background: var(--web2-card);
|
|
||||||
border: 1px solid var(--web2-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1.5rem 1.75rem;
|
|
||||||
}
|
|
||||||
.web2-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid var(--web2-border);
|
|
||||||
color: var(--web2-muted);
|
|
||||||
padding: 0.2rem 0.6rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
.web2-link {
|
|
||||||
color: var(--web2-blue);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.web2-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.web2-button {
|
|
||||||
background: var(--web2-blue);
|
|
||||||
color: #fff;
|
|
||||||
padding: 0.45rem 0.9rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid #1482d0;
|
|
||||||
box-shadow: none;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.web2-button:hover {
|
|
||||||
background: #1787d4;
|
|
||||||
}
|
|
||||||
.web2-button-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.web2-button-group .web2-button {
|
|
||||||
margin: 0 0.5rem 0.5rem 0;
|
|
||||||
}
|
|
||||||
.web2-list li {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid var(--web2-border);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
.web2-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.web2-table thead th {
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.75rem 0.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--web2-muted);
|
|
||||||
border-bottom: 1px solid var(--web2-border);
|
|
||||||
}
|
|
||||||
.web2-table tbody td {
|
|
||||||
padding: 0.9rem 0.5rem;
|
|
||||||
border-bottom: 1px solid var(--web2-border);
|
|
||||||
}
|
|
||||||
.web2-table tbody tr:nth-child(odd) {
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
.web2-table tbody tr:nth-child(even) {
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
.web2-group-row td {
|
|
||||||
background: #e8eef5;
|
|
||||||
color: #0f172a;
|
|
||||||
border-bottom: 1px solid var(--web2-border);
|
|
||||||
padding: 0.65rem 0.5rem;
|
|
||||||
}
|
|
||||||
.web2-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
border: 1px solid var(--web2-border);
|
|
||||||
padding: 0.15rem 0.45rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--web2-muted);
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func Header() templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" rel=\"stylesheet\"><style>\n\t\t\t:root {\n\t\t\t\t--web2-blue: #1d9bf0;\n\t\t\t\t--web2-slate: #0f172a;\n\t\t\t\t--web2-muted: #64748b;\n\t\t\t\t--web2-card: #ffffff;\n\t\t\t\t--web2-border: #e5e7eb;\n\t\t\t}\n\t\t\tbody {\n\t\t\t\tfont-family: \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif;\n\t\t\t\tcolor: var(--web2-slate);\n\t\t\t}\n\t\t\t.web2-bg {\n\t\t\t\tbackground: #ffffff;\n\t\t\t}\n\t\t\t.web2-shell {\n\t\t\t\tmax-width: 1100px;\n\t\t\t\tmargin: 0 auto;\n\t\t\t\tpadding: 2rem 1.5rem 4rem;\n\t\t\t}\n\t\t\t.web2-header {\n\t\t\t\tbackground: var(--web2-card);\n\t\t\t\tborder: 1px solid var(--web2-border);\n\t\t\t\tborder-radius: 4px;\n\t\t\t\tpadding: 1.5rem 2rem;\n\t\t\t}\n\t\t\t.web2-card {\n\t\t\t\tbackground: var(--web2-card);\n\t\t\t\tborder: 1px solid var(--web2-border);\n\t\t\t\tborder-radius: 4px;\n\t\t\t\tpadding: 1.5rem 1.75rem;\n\t\t\t}\n\t\t\t.web2-pill {\n\t\t\t\tdisplay: inline-flex;\n\t\t\t\talign-items: center;\n\t\t\t\tgap: 0.4rem;\n\t\t\t\tbackground: #f8fafc;\n\t\t\t\tborder: 1px solid var(--web2-border);\n\t\t\t\tcolor: var(--web2-muted);\n\t\t\t\tpadding: 0.2rem 0.6rem;\n\t\t\t\tborder-radius: 3px;\n\t\t\t\tfont-size: 0.85rem;\n\t\t\t\tletter-spacing: 0.02em;\n\t\t\t}\n\t\t\t.web2-link {\n\t\t\t\tcolor: var(--web2-blue);\n\t\t\t\ttext-decoration: none;\n\t\t\t\tfont-weight: 600;\n\t\t\t}\n\t\t\t.web2-link:hover {\n\t\t\t\ttext-decoration: underline;\n\t\t\t}\n\t\t\t.web2-button {\n\t\t\t\tbackground: var(--web2-blue);\n\t\t\t\tcolor: #fff;\n\t\t\t\tpadding: 0.45rem 0.9rem;\n\t\t\t\tborder-radius: 3px;\n\t\t\t\tborder: 1px solid #1482d0;\n\t\t\t\tbox-shadow: none;\n\t\t\t\tfont-weight: 600;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\t\t\t.web2-button:hover {\n\t\t\t\tbackground: #1787d4;\n\t\t\t}\n\t\t\t.web2-button-group {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-wrap: wrap;\n\t\t\t}\n\t\t\t.web2-button-group .web2-button {\n\t\t\t\tmargin: 0 0.5rem 0.5rem 0;\n\t\t\t}\n\t\t\t.web2-list li {\n\t\t\t\tbackground: #ffffff;\n\t\t\t\tborder: 1px solid var(--web2-border);\n\t\t\t\tborder-radius: 3px;\n\t\t\t\tpadding: 0.75rem 1rem;\n\t\t\t\tbox-shadow: none;\n\t\t\t}\n\t\t\t.web2-table {\n\t\t\t\twidth: 100%;\n\t\t\t\tborder-collapse: collapse;\n\t\t\t\tfont-size: 0.95rem;\n\t\t\t}\n\t\t\t.web2-table thead th {\n\t\t\t\ttext-align: left;\n\t\t\t\tpadding: 0.75rem 0.5rem;\n\t\t\t\tfont-weight: 700;\n\t\t\t\tcolor: var(--web2-muted);\n\t\t\t\tborder-bottom: 1px solid var(--web2-border);\n\t\t\t}\n\t\t\t.web2-table tbody td {\n\t\t\t\tpadding: 0.9rem 0.5rem;\n\t\t\t\tborder-bottom: 1px solid var(--web2-border);\n\t\t\t}\n\t\t\t.web2-table tbody tr:nth-child(odd) {\n\t\t\t\tbackground: #f8fafc;\n\t\t\t}\n\t\t\t.web2-table tbody tr:nth-child(even) {\n\t\t\t\tbackground: #ffffff;\n\t\t\t}\n\t\t\t.web2-group-row td {\n\t\t\t\tbackground: #e8eef5;\n\t\t\t\tcolor: #0f172a;\n\t\t\t\tborder-bottom: 1px solid var(--web2-border);\n\t\t\t\tpadding: 0.65rem 0.5rem;\n\t\t\t}\n\t\t\t.web2-badge {\n\t\t\t\tdisplay: inline-flex;\n\t\t\t\talign-items: center;\n\t\t\t\tgap: 0.25rem;\n\t\t\t\tborder: 1px solid var(--web2-border);\n\t\t\t\tpadding: 0.15rem 0.45rem;\n\t\t\t\tborder-radius: 3px;\n\t\t\t\tfont-size: 0.8rem;\n\t\t\t\tcolor: var(--web2-muted);\n\t\t\t\tbackground: #f8fafc;\n\t\t\t}\n\t\t</style></head>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" rel=\"stylesheet\"><link href=\"/assets/css/web3.css\" rel=\"stylesheet\"></head>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ templ Index(info BuildInfo) {
|
|||||||
<a class="web2-button" href="/snapshots/hourly">Hourly Snapshots</a>
|
<a class="web2-button" href="/snapshots/hourly">Hourly Snapshots</a>
|
||||||
<a class="web2-button" href="/snapshots/daily">Daily Snapshots</a>
|
<a class="web2-button" href="/snapshots/daily">Daily Snapshots</a>
|
||||||
<a class="web2-button" href="/snapshots/monthly">Monthly Snapshots</a>
|
<a class="web2-button" href="/snapshots/monthly">Monthly Snapshots</a>
|
||||||
|
<a class="web2-button" href="/vcenters">vCenters</a>
|
||||||
<a class="web2-button" href="/swagger/">Swagger UI</a>
|
<a class="web2-button" href="/swagger/">Swagger UI</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,14 +47,14 @@ func Index(info BuildInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCTP Console</div><h1 class=\"mt-3 text-4xl font-bold\">Chargeback Intelligence Dashboard</h1><p class=\"mt-2 text-sm text-slate-600\">Point in time snapshots of consumption.</p></div><div class=\"web2-button-group\"><a class=\"web2-button\" href=\"/snapshots/hourly\">Hourly Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/daily\">Daily Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/monthly\">Monthly Snapshots</a> <a class=\"web2-button\" href=\"/swagger/\">Swagger UI</a></div></div></section><section class=\"grid gap-6 md:grid-cols-3\"><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.2em] text-slate-400\">Build Time</p><p class=\"mt-3 text-xl font-semibold\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCTP Console</div><h1 class=\"mt-3 text-4xl font-bold\">Chargeback Intelligence Dashboard</h1><p class=\"mt-2 text-sm text-slate-600\">Point in time snapshots of consumption.</p></div><div class=\"web2-button-group\"><a class=\"web2-button\" href=\"/snapshots/hourly\">Hourly Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/daily\">Daily Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/monthly\">Monthly Snapshots</a> <a class=\"web2-button\" href=\"/vcenters\">vCenters</a> <a class=\"web2-button\" href=\"/swagger/\">Swagger UI</a></div></div></section><section class=\"grid gap-6 md:grid-cols-3\"><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.2em] text-slate-400\">Build Time</p><p class=\"mt-3 text-xl font-semibold\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(info.BuildTime)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(info.BuildTime)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 38, Col: 59}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 39, Col: 59}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -67,7 +67,7 @@ func Index(info BuildInfo) templ.Component {
|
|||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(info.SHA1Ver)
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(info.SHA1Ver)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 42, Col: 57}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 43, Col: 57}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -80,7 +80,7 @@ func Index(info BuildInfo) templ.Component {
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(info.GoVersion)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(info.GoVersion)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 46, Col: 59}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 47, Col: 59}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"vctp/components/core"
|
"vctp/components/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,6 +12,47 @@ type SnapshotEntry struct {
|
|||||||
Group string
|
Group string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VcenterLink struct {
|
||||||
|
Name string
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VcenterTotalsEntry struct {
|
||||||
|
Snapshot string
|
||||||
|
RawTime int64
|
||||||
|
VmCount int64
|
||||||
|
VcpuTotal int64
|
||||||
|
RamTotalGB int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type VcenterTotalsMeta struct {
|
||||||
|
ViewType string
|
||||||
|
TypeLabel string
|
||||||
|
HourlyLink string
|
||||||
|
DailyLink string
|
||||||
|
MonthlyLink string
|
||||||
|
HourlyClass string
|
||||||
|
DailyClass string
|
||||||
|
MonthlyClass string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VcenterChartData struct {
|
||||||
|
PointsVm string
|
||||||
|
PointsVcpu string
|
||||||
|
PointsRam string
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
GridX []float64
|
||||||
|
GridY []float64
|
||||||
|
YTicks []ChartTick
|
||||||
|
XTicks []ChartTick
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartTick struct {
|
||||||
|
Pos float64
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
||||||
templ SnapshotHourlyList(entries []SnapshotEntry) {
|
templ SnapshotHourlyList(entries []SnapshotEntry) {
|
||||||
@SnapshotListPage("Hourly Inventory Snapshots", "inventory snapshots captured hourly", entries)
|
@SnapshotListPage("Hourly Inventory Snapshots", "inventory snapshots captured hourly", entries)
|
||||||
}
|
}
|
||||||
@@ -84,3 +126,160 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
|
|||||||
@core.Footer()
|
@core.Footer()
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ VcenterList(links []VcenterLink) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
@core.Header()
|
||||||
|
<body class="flex flex-col min-h-screen web2-bg">
|
||||||
|
<main class="flex-grow web2-shell space-y-8">
|
||||||
|
<section class="web2-header">
|
||||||
|
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="web2-pill">vCenter Inventory</div>
|
||||||
|
<h1 class="mt-3 text-4xl font-bold">Monitored vCenters</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">Select a vCenter to view snapshot totals over time.</p>
|
||||||
|
</div>
|
||||||
|
<a class="web2-button" href="/">Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="web2-card">
|
||||||
|
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||||
|
<h2 class="text-lg font-semibold">vCenters</h2>
|
||||||
|
<span class="web2-badge">{len(links)} total</span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden border border-slate-200 rounded">
|
||||||
|
<table class="web2-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>vCenter</th>
|
||||||
|
<th class="text-right">Totals</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for _, link := range links {
|
||||||
|
<tr>
|
||||||
|
<td class="font-semibold text-slate-700">{link.Name}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<a class="web2-link" href={link.Link}>View Totals</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
@core.Footer()
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart VcenterChartData, meta VcenterTotalsMeta) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
@core.Header()
|
||||||
|
<body class="flex flex-col min-h-screen web2-bg">
|
||||||
|
<main class="flex-grow web2-shell space-y-8 max-w-screen-2xl mx-auto" style="max-width: 1400px;">
|
||||||
|
<section class="web2-header">
|
||||||
|
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="web2-pill">vCenter Totals</div>
|
||||||
|
<h1 class="mt-3 text-4xl font-bold">Totals for {vcenter}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{meta.TypeLabel} snapshots of VM count, vCPU, and RAM over time.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<a class="web2-button secondary" href="/vcenters">All vCenters</a>
|
||||||
|
<a class="web2-button" href="/">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="web3-button-group mt-8 mb-3">
|
||||||
|
<a class={meta.HourlyClass} href={meta.HourlyLink}>Hourly</a>
|
||||||
|
<a class={meta.DailyClass} href={meta.DailyLink}>Daily</a>
|
||||||
|
<a class={meta.MonthlyClass} href={meta.MonthlyLink}>Monthly</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="web2-card">
|
||||||
|
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||||
|
<h2 class="text-lg font-semibold">{meta.TypeLabel} Snapshots</h2>
|
||||||
|
<span class="web2-badge">{len(entries)} records</span>
|
||||||
|
</div>
|
||||||
|
if chart.PointsVm != "" {
|
||||||
|
<div class="mb-6 overflow-auto">
|
||||||
|
<svg width="100%" height="360" viewBox={"0 0 " + fmt.Sprintf("%d", chart.Width) + " 320"} role="img" aria-label="Totals over time">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grid" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#e2e8f0" stop-opacity="0.6"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="40" y="10" width={fmt.Sprintf("%d", chart.Width-60)} height="220" fill="white" stroke="#e2e8f0"></rect>
|
||||||
|
<!-- grid lines -->
|
||||||
|
<g stroke="#e2e8f0" stroke-width="1" stroke-dasharray="2,4">
|
||||||
|
for _, y := range chart.GridY {
|
||||||
|
<line x1="40" y1={fmt.Sprintf("%.1f", y)} x2={fmt.Sprintf("%d", chart.Width-20)} y2={fmt.Sprintf("%.1f", y)} />
|
||||||
|
}
|
||||||
|
for _, x := range chart.GridX {
|
||||||
|
<line x1={fmt.Sprintf("%.1f", x)} y1="10" x2={fmt.Sprintf("%.1f", x)} y2="230" />
|
||||||
|
}
|
||||||
|
</g>
|
||||||
|
<!-- axes -->
|
||||||
|
<line x1="40" y1="230" x2={fmt.Sprintf("%d", chart.Width-20)} y2="230" stroke="#94a3b8" stroke-width="1.5"></line>
|
||||||
|
<line x1="40" y1="10" x2="40" y2="230" stroke="#94a3b8" stroke-width="1.5"></line>
|
||||||
|
<!-- data -->
|
||||||
|
<polyline points={chart.PointsVm} fill="none" stroke="#2563eb" stroke-width="2.5"></polyline>
|
||||||
|
<polyline points={chart.PointsVcpu} fill="none" stroke="#16a34a" stroke-width="2.5"></polyline>
|
||||||
|
<polyline points={chart.PointsRam} fill="none" stroke="#ea580c" stroke-width="2.5"></polyline>
|
||||||
|
<!-- tick labels -->
|
||||||
|
<g font-size="10" fill="#475569" text-anchor="end">
|
||||||
|
for _, tick := range chart.YTicks {
|
||||||
|
<text x="36" y={fmt.Sprintf("%.1f", tick.Pos+3)}>{tick.Label}</text>
|
||||||
|
}
|
||||||
|
</g>
|
||||||
|
<g font-size="10" fill="#475569" text-anchor="middle">
|
||||||
|
for _, tick := range chart.XTicks {
|
||||||
|
<text x={fmt.Sprintf("%.1f", tick.Pos)} y="244">{tick.Label}</text>
|
||||||
|
}
|
||||||
|
</g>
|
||||||
|
<!-- legend -->
|
||||||
|
<g font-size="12" fill="#475569" transform={"translate(40 300)"}>
|
||||||
|
<rect x="0" y="0" width="14" height="8" fill="#2563eb"></rect><text x="22" y="12">VMs</text>
|
||||||
|
<rect x="90" y="0" width="14" height="8" fill="#16a34a"></rect><text x="112" y="12">vCPU</text>
|
||||||
|
<rect x="180" y="0" width="14" height="8" fill="#ea580c"></rect><text x="202" y="12">RAM (GB)</text>
|
||||||
|
</g>
|
||||||
|
<!-- axis labels -->
|
||||||
|
<text x="15" y="20" transform={"rotate(-90 15 20)"} font-size="12" fill="#475569">Totals</text>
|
||||||
|
<text x={fmt.Sprintf("%d", chart.Width/2)} y="310" font-size="12" fill="#475569">Snapshot sequence (newest right)</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="overflow-hidden border border-slate-200 rounded">
|
||||||
|
<table class="web2-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Snapshot Time</th>
|
||||||
|
<th class="text-right">VMs</th>
|
||||||
|
<th class="text-right">vCPUs</th>
|
||||||
|
<th class="text-right">RAM (GB)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for _, entry := range entries {
|
||||||
|
<tr>
|
||||||
|
<td>{entry.Snapshot}</td>
|
||||||
|
<td class="text-right">{entry.VmCount}</td>
|
||||||
|
<td class="text-right">{entry.VcpuTotal}</td>
|
||||||
|
<td class="text-right">{entry.RamTotalGB}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
@core.Footer()
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import "github.com/a-h/templ"
|
|||||||
import templruntime "github.com/a-h/templ/runtime"
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"vctp/components/core"
|
"vctp/components/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +20,47 @@ type SnapshotEntry struct {
|
|||||||
Group string
|
Group string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VcenterLink struct {
|
||||||
|
Name string
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VcenterTotalsEntry struct {
|
||||||
|
Snapshot string
|
||||||
|
RawTime int64
|
||||||
|
VmCount int64
|
||||||
|
VcpuTotal int64
|
||||||
|
RamTotalGB int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type VcenterTotalsMeta struct {
|
||||||
|
ViewType string
|
||||||
|
TypeLabel string
|
||||||
|
HourlyLink string
|
||||||
|
DailyLink string
|
||||||
|
MonthlyLink string
|
||||||
|
HourlyClass string
|
||||||
|
DailyClass string
|
||||||
|
MonthlyClass string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VcenterChartData struct {
|
||||||
|
PointsVm string
|
||||||
|
PointsVcpu string
|
||||||
|
PointsRam string
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
GridX []float64
|
||||||
|
GridY []float64
|
||||||
|
YTicks []ChartTick
|
||||||
|
XTicks []ChartTick
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartTick struct {
|
||||||
|
Pos float64
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
||||||
func SnapshotHourlyList(entries []SnapshotEntry) templ.Component {
|
func SnapshotHourlyList(entries []SnapshotEntry) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
@@ -142,7 +184,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 36, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 78, Col: 49}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -155,7 +197,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 37, Col: 55}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 79, Col: 55}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -168,7 +210,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 46, Col: 44}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 88, Col: 44}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -187,7 +229,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 61, Col: 76}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 103, Col: 76}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -205,7 +247,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 67, Col: 75}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 109, Col: 75}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -218,7 +260,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var10 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count)
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 71, Col: 48}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 113, Col: 48}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -231,7 +273,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var11 templ.SafeURL
|
var templ_7745c5c3_Var11 templ.SafeURL
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 74, Col: 48}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 116, Col: 48}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -258,4 +300,642 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func VcenterList(links []VcenterLink) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var12 == nil {
|
||||||
|
templ_7745c5c3_Var12 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<!doctype html><html lang=\"en\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = core.Header().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCenter Inventory</div><h1 class=\"mt-3 text-4xl font-bold\">Monitored vCenters</h1><p class=\"mt-2 text-sm text-slate-600\">Select a vCenter to view snapshot totals over time.</p></div><a class=\"web2-button\" href=\"/\">Back to Dashboard</a></div></section><section class=\"web2-card\"><div class=\"flex items-center justify-between gap-3 mb-4 flex-wrap\"><h2 class=\"text-lg font-semibold\">vCenters</h2><span class=\"web2-badge\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(len(links))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 150, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " total</span></div><div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>vCenter</th><th class=\"text-right\">Totals</th></tr></thead> <tbody>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, link := range links {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<tr><td class=\"font-semibold text-slate-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 163, Col: 61}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td class=\"text-right\"><a class=\"web2-link\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 165, Col: 47}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">View Totals</a></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</tbody></table></div></section></main></body>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = core.Footer().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart VcenterChartData, meta VcenterTotalsMeta) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var16 == nil {
|
||||||
|
templ_7745c5c3_Var16 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<!doctype html><html lang=\"en\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = core.Header().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8 max-w-screen-2xl mx-auto\" style=\"max-width: 1400px;\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCenter Totals</div><h1 class=\"mt-3 text-4xl font-bold\">Totals for ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vcenter)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 189, Col: 63}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</h1><p class=\"mt-2 text-sm text-slate-600\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 string
|
||||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 190, Col: 62}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " snapshots of VM count, vCPU, and RAM over time.</p></div><div class=\"flex gap-3\"><a class=\"web2-button secondary\" href=\"/vcenters\">All vCenters</a> <a class=\"web2-button\" href=\"/\">Dashboard</a></div></div><div class=\"web3-button-group mt-8 mb-3\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var19 = []any{meta.HourlyClass}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<a class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var20 string
|
||||||
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var19).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var21 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(meta.HourlyLink)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 198, Col: 56}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">Hourly</a> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 = []any{meta.DailyClass}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<a class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var23 string
|
||||||
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var22).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var24 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(meta.DailyLink)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 199, Col: 54}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">Daily</a> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var25 = []any{meta.MonthlyClass}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var25...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var26 string
|
||||||
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var25).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var27 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(meta.MonthlyLink)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 200, Col: 58}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">Monthly</a></div></section><section class=\"web2-card\"><div class=\"flex items-center justify-between gap-3 mb-4 flex-wrap\"><h2 class=\"text-lg font-semibold\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var28 string
|
||||||
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 206, Col: 56}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " Snapshots</h2><span class=\"web2-badge\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var29 string
|
||||||
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 207, Col: 45}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " records</span></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if chart.PointsVm != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"mb-6 overflow-auto\"><svg width=\"100%\" height=\"360\" viewBox=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var30 string
|
||||||
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs("0 0 " + fmt.Sprintf("%d", chart.Width) + " 320")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 211, Col: 95}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" role=\"img\" aria-label=\"Totals over time\"><defs><linearGradient id=\"grid\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#e2e8f0\" stop-opacity=\"0.6\"></stop></linearGradient></defs> <rect x=\"40\" y=\"10\" width=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var31 string
|
||||||
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-60))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 68}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" height=\"220\" fill=\"white\" stroke=\"#e2e8f0\"></rect><!-- grid lines --><g stroke=\"#e2e8f0\" stroke-width=\"1\" stroke-dasharray=\"2,4\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, y := range chart.GridY {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<line x1=\"40\" y1=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var32 string
|
||||||
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", y))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 221, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" x2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var33 string
|
||||||
|
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-20))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 221, Col: 89}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" y2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var34 string
|
||||||
|
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", y))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 221, Col: 117}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\"></line> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, x := range chart.GridX {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<line x1=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var35 string
|
||||||
|
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", x))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 224, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" y1=\"10\" x2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var36 string
|
||||||
|
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", x))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 224, Col: 78}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" y2=\"230\"></line>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</g><!-- axes --><line x1=\"40\" y1=\"230\" x2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var37 string
|
||||||
|
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-20))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 228, Col: 68}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" y2=\"230\" stroke=\"#94a3b8\" stroke-width=\"1.5\"></line> <line x1=\"40\" y1=\"10\" x2=\"40\" y2=\"230\" stroke=\"#94a3b8\" stroke-width=\"1.5\"></line><!-- data --><polyline points=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var38 string
|
||||||
|
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsVm)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 231, Col: 40}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\" fill=\"none\" stroke=\"#2563eb\" stroke-width=\"2.5\"></polyline> <polyline points=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var39 string
|
||||||
|
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsVcpu)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 232, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\" fill=\"none\" stroke=\"#16a34a\" stroke-width=\"2.5\"></polyline> <polyline points=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var40 string
|
||||||
|
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsRam)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 233, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\" fill=\"none\" stroke=\"#ea580c\" stroke-width=\"2.5\"></polyline><!-- tick labels --><g font-size=\"10\" fill=\"#475569\" text-anchor=\"end\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, tick := range chart.YTicks {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<text x=\"36\" y=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var41 string
|
||||||
|
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", tick.Pos+3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 237, Col: 57}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var42 string
|
||||||
|
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 237, Col: 70}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</text>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</g> <g font-size=\"10\" fill=\"#475569\" text-anchor=\"middle\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, tick := range chart.XTicks {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<text x=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var43 string
|
||||||
|
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", tick.Pos))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 242, Col: 48}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" y=\"244\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var44 string
|
||||||
|
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 242, Col: 69}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</text>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</g><!-- legend --><g font-size=\"12\" fill=\"#475569\" transform=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var45 string
|
||||||
|
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs("translate(40 300)")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 246, Col: 72}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\"><rect x=\"0\" y=\"0\" width=\"14\" height=\"8\" fill=\"#2563eb\"></rect><text x=\"22\" y=\"12\">VMs</text> <rect x=\"90\" y=\"0\" width=\"14\" height=\"8\" fill=\"#16a34a\"></rect><text x=\"112\" y=\"12\">vCPU</text> <rect x=\"180\" y=\"0\" width=\"14\" height=\"8\" fill=\"#ea580c\"></rect><text x=\"202\" y=\"12\">RAM (GB)</text></g><!-- axis labels --><text x=\"15\" y=\"20\" transform=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var46 string
|
||||||
|
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs("rotate(-90 15 20)")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 252, Col: 59}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" font-size=\"12\" fill=\"#475569\">Totals</text> <text x=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var47 string
|
||||||
|
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width/2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 253, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" y=\"310\" font-size=\"12\" fill=\"#475569\">Snapshot sequence (newest right)</text></svg></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>Snapshot Time</th><th class=\"text-right\">VMs</th><th class=\"text-right\">vCPUs</th><th class=\"text-right\">RAM (GB)</th></tr></thead> <tbody>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<tr><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var48 string
|
||||||
|
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 271, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</td><td class=\"text-right\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var49 string
|
||||||
|
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 272, Col: 47}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</td><td class=\"text-right\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var50 string
|
||||||
|
templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 273, Col: 49}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</td><td class=\"text-right\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var51 string
|
||||||
|
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 274, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</tbody></table></div></section></main></body>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = core.Footer().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|||||||
382
db/helpers.go
382
db/helpers.go
@@ -354,6 +354,388 @@ func ApplySQLiteTuning(ctx context.Context, dbConn *sqlx.DB) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureVmIdentityTables creates the identity and rename audit tables.
|
||||||
|
func EnsureVmIdentityTables(ctx context.Context, dbConn *sqlx.DB) error {
|
||||||
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
var identityDDL, renameDDL string
|
||||||
|
switch driver {
|
||||||
|
case "pgx", "postgres":
|
||||||
|
identityDDL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS vm_identity (
|
||||||
|
"VmId" TEXT NOT NULL,
|
||||||
|
"VmUuid" TEXT NOT NULL,
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"Name" TEXT NOT NULL,
|
||||||
|
"Cluster" TEXT,
|
||||||
|
"FirstSeen" BIGINT NOT NULL,
|
||||||
|
"LastSeen" BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY ("VmId","VmUuid","Vcenter")
|
||||||
|
)`
|
||||||
|
renameDDL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS vm_renames (
|
||||||
|
"RowId" BIGSERIAL PRIMARY KEY,
|
||||||
|
"VmId" TEXT NOT NULL,
|
||||||
|
"VmUuid" TEXT NOT NULL,
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"OldName" TEXT,
|
||||||
|
"NewName" TEXT,
|
||||||
|
"OldCluster" TEXT,
|
||||||
|
"NewCluster" TEXT,
|
||||||
|
"SnapshotTime" BIGINT NOT NULL
|
||||||
|
)`
|
||||||
|
default:
|
||||||
|
identityDDL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS vm_identity (
|
||||||
|
"VmId" TEXT NOT NULL,
|
||||||
|
"VmUuid" TEXT NOT NULL,
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"Name" TEXT NOT NULL,
|
||||||
|
"Cluster" TEXT,
|
||||||
|
"FirstSeen" BIGINT NOT NULL,
|
||||||
|
"LastSeen" BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY ("VmId","VmUuid","Vcenter")
|
||||||
|
)`
|
||||||
|
renameDDL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS vm_renames (
|
||||||
|
"RowId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"VmId" TEXT NOT NULL,
|
||||||
|
"VmUuid" TEXT NOT NULL,
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"OldName" TEXT,
|
||||||
|
"NewName" TEXT,
|
||||||
|
"OldCluster" TEXT,
|
||||||
|
"NewCluster" TEXT,
|
||||||
|
"SnapshotTime" BIGINT NOT NULL
|
||||||
|
)`
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, identityDDL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, renameDDL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
indexes := []string{
|
||||||
|
`CREATE INDEX IF NOT EXISTS vm_identity_vcenter_idx ON vm_identity ("Vcenter")`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS vm_identity_uuid_idx ON vm_identity ("VmUuid","Vcenter")`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS vm_identity_name_idx ON vm_identity ("Name","Vcenter")`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS vm_renames_vcenter_idx ON vm_renames ("Vcenter","SnapshotTime")`,
|
||||||
|
}
|
||||||
|
for _, idx := range indexes {
|
||||||
|
if _, err := dbConn.ExecContext(ctx, idx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertVmIdentity updates/creates the identity record and records rename events.
|
||||||
|
func UpsertVmIdentity(ctx context.Context, dbConn *sqlx.DB, vcenter string, vmId, vmUuid sql.NullString, name string, cluster sql.NullString, snapshotTime time.Time) error {
|
||||||
|
keyVmID := strings.TrimSpace(vmId.String)
|
||||||
|
keyUuid := strings.TrimSpace(vmUuid.String)
|
||||||
|
if keyVmID == "" || keyUuid == "" || strings.TrimSpace(vcenter) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := EnsureVmIdentityTables(ctx, dbConn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type identityRow struct {
|
||||||
|
Name string `db:"Name"`
|
||||||
|
Cluster sql.NullString `db:"Cluster"`
|
||||||
|
FirstSeen sql.NullInt64 `db:"FirstSeen"`
|
||||||
|
LastSeen sql.NullInt64 `db:"LastSeen"`
|
||||||
|
}
|
||||||
|
var existing identityRow
|
||||||
|
err := dbConn.GetContext(ctx, &existing, `
|
||||||
|
SELECT "Name","Cluster","FirstSeen","LastSeen"
|
||||||
|
FROM vm_identity
|
||||||
|
WHERE "Vcenter" = $1 AND "VmId" = $2 AND "VmUuid" = $3
|
||||||
|
`, vcenter, keyVmID, keyUuid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(strings.ToLower(err.Error()), "no rows") {
|
||||||
|
_, err = dbConn.ExecContext(ctx, `
|
||||||
|
INSERT INTO vm_identity ("VmId","VmUuid","Vcenter","Name","Cluster","FirstSeen","LastSeen")
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$6)
|
||||||
|
`, keyVmID, keyUuid, vcenter, name, nullString(cluster), snapshotTime.Unix())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
renamed := !strings.EqualFold(existing.Name, name) || !strings.EqualFold(strings.TrimSpace(existing.Cluster.String), strings.TrimSpace(cluster.String))
|
||||||
|
if renamed {
|
||||||
|
_, _ = dbConn.ExecContext(ctx, `
|
||||||
|
INSERT INTO vm_renames ("VmId","VmUuid","Vcenter","OldName","NewName","OldCluster","NewCluster","SnapshotTime")
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||||
|
`, keyVmID, keyUuid, vcenter, existing.Name, name, existing.Cluster.String, cluster.String, snapshotTime.Unix())
|
||||||
|
}
|
||||||
|
_, err = dbConn.ExecContext(ctx, `
|
||||||
|
UPDATE vm_identity
|
||||||
|
SET "Name" = $1, "Cluster" = $2, "LastSeen" = $3
|
||||||
|
WHERE "Vcenter" = $4 AND "VmId" = $5 AND "VmUuid" = $6
|
||||||
|
`, name, nullString(cluster), snapshotTime.Unix(), vcenter, keyVmID, keyUuid)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullString(val sql.NullString) interface{} {
|
||||||
|
if val.Valid {
|
||||||
|
return val.String
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureVcenterTotalsTable creates the vcenter_totals table if missing.
|
||||||
|
func EnsureVcenterTotalsTable(ctx context.Context, dbConn *sqlx.DB) error {
|
||||||
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
var ddl string
|
||||||
|
switch driver {
|
||||||
|
case "pgx", "postgres":
|
||||||
|
ddl = `
|
||||||
|
CREATE TABLE IF NOT EXISTS vcenter_totals (
|
||||||
|
"RowId" BIGSERIAL PRIMARY KEY,
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"SnapshotTime" BIGINT NOT NULL,
|
||||||
|
"VmCount" BIGINT NOT NULL,
|
||||||
|
"VcpuTotal" BIGINT NOT NULL,
|
||||||
|
"RamTotalGB" BIGINT NOT NULL
|
||||||
|
);`
|
||||||
|
default:
|
||||||
|
ddl = `
|
||||||
|
CREATE TABLE IF NOT EXISTS vcenter_totals (
|
||||||
|
"RowId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"SnapshotTime" BIGINT NOT NULL,
|
||||||
|
"VmCount" BIGINT NOT NULL,
|
||||||
|
"VcpuTotal" BIGINT NOT NULL,
|
||||||
|
"RamTotalGB" BIGINT NOT NULL
|
||||||
|
);`
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, ddl); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
indexes := []string{
|
||||||
|
`CREATE INDEX IF NOT EXISTS vcenter_totals_vc_time_idx ON vcenter_totals ("Vcenter","SnapshotTime" DESC)`,
|
||||||
|
}
|
||||||
|
for _, idx := range indexes {
|
||||||
|
if _, err := dbConn.ExecContext(ctx, idx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertVcenterTotals records totals for a vcenter at a snapshot time.
|
||||||
|
func InsertVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotTime time.Time, vmCount, vcpuTotal, ramTotal int64) error {
|
||||||
|
if strings.TrimSpace(vcenter) == "" {
|
||||||
|
return fmt.Errorf("vcenter is empty")
|
||||||
|
}
|
||||||
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := dbConn.ExecContext(ctx, `
|
||||||
|
INSERT INTO vcenter_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
||||||
|
VALUES ($1,$2,$3,$4,$5)
|
||||||
|
`, vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVcenters returns distinct vcenter URLs tracked.
|
||||||
|
func ListVcenters(ctx context.Context, dbConn *sqlx.DB) ([]string, error) {
|
||||||
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows, err := dbConn.QueryxContext(ctx, `SELECT DISTINCT "Vcenter" FROM vcenter_totals ORDER BY "Vcenter"`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []string
|
||||||
|
for rows.Next() {
|
||||||
|
var v string
|
||||||
|
if err := rows.Scan(&v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// VcenterTotalRow holds per-snapshot totals for a vcenter.
|
||||||
|
type VcenterTotalRow struct {
|
||||||
|
SnapshotTime int64 `db:"SnapshotTime"`
|
||||||
|
Vcenter string `db:"Vcenter"`
|
||||||
|
VmCount int64 `db:"VmCount"`
|
||||||
|
VcpuTotal int64 `db:"VcpuTotal"`
|
||||||
|
RamTotalGB int64 `db:"RamTotalGB"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVcenterTotals lists totals for a vcenter sorted by snapshot_time desc, limited.
|
||||||
|
func ListVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, limit int) ([]VcenterTotalRow, error) {
|
||||||
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
rows := make([]VcenterTotalRow, 0, limit)
|
||||||
|
query := `
|
||||||
|
SELECT "Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB"
|
||||||
|
FROM vcenter_totals
|
||||||
|
WHERE "Vcenter" = $1
|
||||||
|
ORDER BY "SnapshotTime" DESC
|
||||||
|
LIMIT $2`
|
||||||
|
if err := dbConn.SelectContext(ctx, &rows, query, vcenter, limit); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVcenterTotalsByType returns totals for a vcenter for the requested snapshot type (hourly, daily, monthly).
|
||||||
|
// Hourly values come from vcenter_totals; daily/monthly are derived from the summary tables referenced in snapshot_registry.
|
||||||
|
func ListVcenterTotalsByType(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotType string, limit int) ([]VcenterTotalRow, error) {
|
||||||
|
snapshotType = strings.ToLower(snapshotType)
|
||||||
|
if snapshotType == "" {
|
||||||
|
snapshotType = "hourly"
|
||||||
|
}
|
||||||
|
if snapshotType == "hourly" {
|
||||||
|
return ListVcenterTotals(ctx, dbConn, vcenter, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
query := `
|
||||||
|
SELECT table_name, snapshot_time
|
||||||
|
FROM snapshot_registry
|
||||||
|
WHERE snapshot_type = $1
|
||||||
|
ORDER BY snapshot_time DESC
|
||||||
|
LIMIT $2
|
||||||
|
`
|
||||||
|
if driver == "sqlite" {
|
||||||
|
query = strings.ReplaceAll(query, "$1", "?")
|
||||||
|
query = strings.ReplaceAll(query, "$2", "?")
|
||||||
|
}
|
||||||
|
|
||||||
|
var regRows []struct {
|
||||||
|
TableName string `db:"table_name"`
|
||||||
|
SnapshotTime int64 `db:"snapshot_time"`
|
||||||
|
}
|
||||||
|
if err := dbConn.SelectContext(ctx, ®Rows, query, snapshotType, limit); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]VcenterTotalRow, 0, len(regRows))
|
||||||
|
for _, r := range regRows {
|
||||||
|
if err := ValidateTableName(r.TableName); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
agg, err := aggregateSummaryTotals(ctx, dbConn, r.TableName, vcenter)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, VcenterTotalRow{
|
||||||
|
SnapshotTime: r.SnapshotTime,
|
||||||
|
Vcenter: vcenter,
|
||||||
|
VmCount: agg.VmCount,
|
||||||
|
VcpuTotal: agg.VcpuTotal,
|
||||||
|
RamTotalGB: agg.RamTotalGB,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type summaryAgg struct {
|
||||||
|
VmCount int64 `db:"vm_count"`
|
||||||
|
VcpuTotal int64 `db:"vcpu_total"`
|
||||||
|
RamTotalGB int64 `db:"ram_total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// aggregateSummaryTotals computes totals for a single summary table (daily/monthly) for a given vcenter.
|
||||||
|
func aggregateSummaryTotals(ctx context.Context, dbConn *sqlx.DB, tableName string, vcenter string) (summaryAgg, error) {
|
||||||
|
if _, err := SafeTableName(tableName); err != nil {
|
||||||
|
return summaryAgg{}, err
|
||||||
|
}
|
||||||
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
COUNT(1) AS vm_count,
|
||||||
|
COALESCE(SUM(COALESCE("AvgVcpuCount","VcpuCount")),0) AS vcpu_total,
|
||||||
|
COALESCE(SUM(COALESCE("AvgRamGB","RamGB")),0) AS ram_total
|
||||||
|
FROM %s
|
||||||
|
WHERE "Vcenter" = $1
|
||||||
|
`, tableName)
|
||||||
|
if driver == "sqlite" {
|
||||||
|
query = strings.ReplaceAll(query, "$1", "?")
|
||||||
|
}
|
||||||
|
var agg summaryAgg
|
||||||
|
if err := dbConn.GetContext(ctx, &agg, query, vcenter); err != nil {
|
||||||
|
return summaryAgg{}, err
|
||||||
|
}
|
||||||
|
return agg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncVcenterTotalsFromSnapshots backfills vcenter_totals using hourly snapshot tables in snapshot_registry.
|
||||||
|
func SyncVcenterTotalsFromSnapshots(ctx context.Context, dbConn *sqlx.DB) error {
|
||||||
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
var hourlyTables []struct {
|
||||||
|
TableName string `db:"table_name"`
|
||||||
|
SnapshotTime int64 `db:"snapshot_time"`
|
||||||
|
}
|
||||||
|
if err := dbConn.SelectContext(ctx, &hourlyTables, `
|
||||||
|
SELECT table_name, snapshot_time
|
||||||
|
FROM snapshot_registry
|
||||||
|
WHERE snapshot_type = 'hourly'
|
||||||
|
ORDER BY snapshot_time
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, ht := range hourlyTables {
|
||||||
|
if err := ValidateTableName(ht.TableName); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Aggregate per vcenter from the snapshot table.
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT "Vcenter" AS vcenter,
|
||||||
|
COUNT(1) AS vm_count,
|
||||||
|
COALESCE(SUM(CASE WHEN "VcpuCount" IS NOT NULL THEN "VcpuCount" ELSE 0 END), 0) AS vcpu_total,
|
||||||
|
COALESCE(SUM(CASE WHEN "RamGB" IS NOT NULL THEN "RamGB" ELSE 0 END), 0) AS ram_total
|
||||||
|
FROM %s
|
||||||
|
GROUP BY "Vcenter"
|
||||||
|
`, ht.TableName)
|
||||||
|
type aggRow struct {
|
||||||
|
Vcenter string `db:"vcenter"`
|
||||||
|
VmCount int64 `db:"vm_count"`
|
||||||
|
VcpuTotal int64 `db:"vcpu_total"`
|
||||||
|
RamTotal int64 `db:"ram_total"`
|
||||||
|
}
|
||||||
|
var aggs []aggRow
|
||||||
|
if err := dbConn.SelectContext(ctx, &aggs, query); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, a := range aggs {
|
||||||
|
// Insert if missing.
|
||||||
|
insert := `
|
||||||
|
INSERT INTO vcenter_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
||||||
|
SELECT $1,$2,$3,$4,$5
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM vcenter_totals WHERE "Vcenter" = $1 AND "SnapshotTime" = $2
|
||||||
|
)
|
||||||
|
`
|
||||||
|
if driver == "sqlite" {
|
||||||
|
insert = strings.ReplaceAll(insert, "$", "?")
|
||||||
|
}
|
||||||
|
_, _ = dbConn.ExecContext(ctx, insert, a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AnalyzeTableIfPostgres runs ANALYZE on a table to refresh planner stats.
|
// AnalyzeTableIfPostgres runs ANALYZE on a table to refresh planner stats.
|
||||||
func AnalyzeTableIfPostgres(ctx context.Context, dbConn *sqlx.DB, tableName string) {
|
func AnalyzeTableIfPostgres(ctx context.Context, dbConn *sqlx.DB, tableName string) {
|
||||||
if _, err := SafeTableName(tableName); err != nil {
|
if _, err := SafeTableName(tableName); err != nil {
|
||||||
|
|||||||
146
dist/assets/css/web3.css
vendored
Normal file
146
dist/assets/css/web3.css
vendored
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
:root {
|
||||||
|
--web2-blue: #1d9bf0;
|
||||||
|
--web2-slate: #0f172a;
|
||||||
|
--web2-muted: #64748b;
|
||||||
|
--web2-card: #ffffff;
|
||||||
|
--web2-border: #e5e7eb;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
color: var(--web2-slate);
|
||||||
|
}
|
||||||
|
.web2-bg {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
.web2-shell {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1.5rem 4rem;
|
||||||
|
}
|
||||||
|
.web2-header {
|
||||||
|
background: var(--web2-card);
|
||||||
|
border: 1px solid var(--web2-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
}
|
||||||
|
.web2-card {
|
||||||
|
background: var(--web2-card);
|
||||||
|
border: 1px solid var(--web2-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1.5rem 1.75rem;
|
||||||
|
}
|
||||||
|
.web2-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid var(--web2-border);
|
||||||
|
color: var(--web2-muted);
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.web2-link {
|
||||||
|
color: var(--web2-blue);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.web2-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.web2-button {
|
||||||
|
background: var(--web2-blue);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.45rem 0.9rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #1482d0;
|
||||||
|
box-shadow: none;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.web2-button:hover {
|
||||||
|
background: #1787d4;
|
||||||
|
}
|
||||||
|
.web2-button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.web2-button-group .web2-button {
|
||||||
|
margin: 0 0.5rem 0.5rem 0;
|
||||||
|
}
|
||||||
|
.web3-button {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #0f172a;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
.web3-button:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.web3-button.active {
|
||||||
|
background: #dbeafe;
|
||||||
|
border-color: #93c5fd;
|
||||||
|
color: #1d4ed8;
|
||||||
|
box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.35);
|
||||||
|
}
|
||||||
|
.web3-button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.web2-list li {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid var(--web2-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.web2-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.web2-table thead th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--web2-muted);
|
||||||
|
border-bottom: 1px solid var(--web2-border);
|
||||||
|
}
|
||||||
|
.web2-table tbody td {
|
||||||
|
padding: 0.9rem 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--web2-border);
|
||||||
|
}
|
||||||
|
.web2-table tbody tr:nth-child(odd) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.web2-table tbody tr:nth-child(even) {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
.web2-group-row td {
|
||||||
|
background: #e8eef5;
|
||||||
|
color: #0f172a;
|
||||||
|
border-bottom: 1px solid var(--web2-border);
|
||||||
|
padding: 0.65rem 0.5rem;
|
||||||
|
}
|
||||||
|
.web2-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
border: 1px solid var(--web2-border);
|
||||||
|
padding: 0.15rem 0.45rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--web2-muted);
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
@@ -824,6 +824,9 @@ func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTim
|
|||||||
return fmt.Errorf("unable to get VMs from vcenter: %w", err)
|
return fmt.Errorf("unable to get VMs from vcenter: %w", err)
|
||||||
}
|
}
|
||||||
c.Logger.Debug("retrieved VMs from vcenter", "url", url, "vm_count", len(vcVms))
|
c.Logger.Debug("retrieved VMs from vcenter", "url", url, "vm_count", len(vcVms))
|
||||||
|
if err := db.EnsureVmIdentityTables(ctx, c.Database.DB()); err != nil {
|
||||||
|
c.Logger.Warn("failed to ensure vm identity tables", "error", err)
|
||||||
|
}
|
||||||
hostLookup, err := vc.BuildHostLookup()
|
hostLookup, err := vc.BuildHostLookup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger.Warn("failed to build host lookup", "url", url, "error", err)
|
c.Logger.Warn("failed to build host lookup", "url", url, "error", err)
|
||||||
@@ -892,6 +895,7 @@ func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTim
|
|||||||
c.Logger.Error("unable to build snapshot for VM", "vm_id", vm.Reference().Value, "error", err)
|
c.Logger.Error("unable to build snapshot for VM", "vm_id", vm.Reference().Value, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
_ = db.UpsertVmIdentity(ctx, dbConn, url, row.VmId, row.VmUuid, row.Name, row.Cluster, startTime)
|
||||||
presentSnapshots[vm.Reference().Value] = row
|
presentSnapshots[vm.Reference().Value] = row
|
||||||
if row.VmUuid.Valid {
|
if row.VmUuid.Valid {
|
||||||
presentByUuid[row.VmUuid.String] = struct{}{}
|
presentByUuid[row.VmUuid.String] = struct{}{}
|
||||||
@@ -971,6 +975,8 @@ func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTim
|
|||||||
_ = db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, false, err.Error())
|
_ = db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, false, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Record per-vCenter totals snapshot.
|
||||||
|
_ = db.InsertVcenterTotals(ctx, dbConn, url, startTime, totals.VmCount, totals.VcpuTotal, totals.RamTotal)
|
||||||
|
|
||||||
// Compare with previous snapshot for this vcenter to mark deletions at snapshot time.
|
// Compare with previous snapshot for this vcenter to mark deletions at snapshot time.
|
||||||
if prevTable, err := latestHourlySnapshotBefore(ctx, dbConn, startTime); err == nil && prevTable != "" {
|
if prevTable, err := latestHourlySnapshotBefore(ctx, dbConn, startTime); err == nil && prevTable != "" {
|
||||||
@@ -1084,13 +1090,14 @@ func (c *CronTask) markMissingFromPrevious(ctx context.Context, dbConn *sqlx.DB,
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf(`SELECT "VmId","VmUuid","Name","Datacenter","DeletionTime" FROM %s WHERE "Vcenter" = ?`, prevTable)
|
query := fmt.Sprintf(`SELECT "VmId","VmUuid","Name","Cluster","Datacenter","DeletionTime" FROM %s WHERE "Vcenter" = ?`, prevTable)
|
||||||
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
|
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
|
||||||
|
|
||||||
type prevRow struct {
|
type prevRow struct {
|
||||||
VmId sql.NullString `db:"VmId"`
|
VmId sql.NullString `db:"VmId"`
|
||||||
VmUuid sql.NullString `db:"VmUuid"`
|
VmUuid sql.NullString `db:"VmUuid"`
|
||||||
Name string `db:"Name"`
|
Name string `db:"Name"`
|
||||||
|
Cluster sql.NullString `db:"Cluster"`
|
||||||
Datacenter sql.NullString `db:"Datacenter"`
|
Datacenter sql.NullString `db:"Datacenter"`
|
||||||
DeletionTime sql.NullInt64 `db:"DeletionTime"`
|
DeletionTime sql.NullInt64 `db:"DeletionTime"`
|
||||||
}
|
}
|
||||||
@@ -1111,6 +1118,7 @@ func (c *CronTask) markMissingFromPrevious(ctx context.Context, dbConn *sqlx.DB,
|
|||||||
vmID := r.VmId.String
|
vmID := r.VmId.String
|
||||||
uuid := r.VmUuid.String
|
uuid := r.VmUuid.String
|
||||||
name := r.Name
|
name := r.Name
|
||||||
|
cluster := r.Cluster.String
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
if vmID != "" {
|
if vmID != "" {
|
||||||
@@ -1128,6 +1136,12 @@ func (c *CronTask) markMissingFromPrevious(ctx context.Context, dbConn *sqlx.DB,
|
|||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If the name is missing but UUID+Cluster still exists in inventory/current, treat it as present (rename, not delete).
|
||||||
|
if !found && uuid != "" && cluster != "" {
|
||||||
|
if inv, ok := invByUuid[uuid]; ok && strings.EqualFold(inv.Cluster.String, cluster) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
if found {
|
if found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
225
server/handler/vcenters.go
Normal file
225
server/handler/vcenters.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -854,6 +854,73 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/vcenters": {
|
||||||
|
"get": {
|
||||||
|
"description": "Lists all vCenters with recorded snapshot totals.",
|
||||||
|
"produces": [
|
||||||
|
"text/html"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"vcenters"
|
||||||
|
],
|
||||||
|
"summary": "List vCenters",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "HTML page",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/vcenters/totals": {
|
||||||
|
"get": {
|
||||||
|
"description": "Shows per-snapshot totals for a vCenter.",
|
||||||
|
"produces": [
|
||||||
|
"text/html"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"vcenters"
|
||||||
|
],
|
||||||
|
"summary": "vCenter totals",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "vCenter URL",
|
||||||
|
"name": "vcenter",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "hourly|daily|monthly (default: hourly)",
|
||||||
|
"name": "type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit results (default 200)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "HTML page",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Missing vcenter",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
|||||||
@@ -843,6 +843,73 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/vcenters": {
|
||||||
|
"get": {
|
||||||
|
"description": "Lists all vCenters with recorded snapshot totals.",
|
||||||
|
"produces": [
|
||||||
|
"text/html"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"vcenters"
|
||||||
|
],
|
||||||
|
"summary": "List vCenters",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "HTML page",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/vcenters/totals": {
|
||||||
|
"get": {
|
||||||
|
"description": "Shows per-snapshot totals for a vCenter.",
|
||||||
|
"produces": [
|
||||||
|
"text/html"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"vcenters"
|
||||||
|
],
|
||||||
|
"summary": "vCenter totals",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "vCenter URL",
|
||||||
|
"name": "vcenter",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "hourly|daily|monthly (default: hourly)",
|
||||||
|
"name": "type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit results (default 200)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "HTML page",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Missing vcenter",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
|||||||
@@ -720,4 +720,48 @@ paths:
|
|||||||
summary: List monthly snapshots
|
summary: List monthly snapshots
|
||||||
tags:
|
tags:
|
||||||
- snapshots
|
- snapshots
|
||||||
|
/vcenters:
|
||||||
|
get:
|
||||||
|
description: Lists all vCenters with recorded snapshot totals.
|
||||||
|
produces:
|
||||||
|
- text/html
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: HTML page
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: List vCenters
|
||||||
|
tags:
|
||||||
|
- vcenters
|
||||||
|
/vcenters/totals:
|
||||||
|
get:
|
||||||
|
description: Shows per-snapshot totals for a vCenter.
|
||||||
|
parameters:
|
||||||
|
- description: vCenter URL
|
||||||
|
in: query
|
||||||
|
name: vcenter
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: 'hourly|daily|monthly (default: hourly)'
|
||||||
|
in: query
|
||||||
|
name: type
|
||||||
|
type: string
|
||||||
|
- description: Limit results (default 200)
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- text/html
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: HTML page
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: Missing vcenter
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: vCenter totals
|
||||||
|
tags:
|
||||||
|
- vcenters
|
||||||
swagger: "2.0"
|
swagger: "2.0"
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
|
|||||||
mux.HandleFunc("/api/snapshots/hourly/force", h.SnapshotForceHourly)
|
mux.HandleFunc("/api/snapshots/hourly/force", h.SnapshotForceHourly)
|
||||||
mux.HandleFunc("/api/snapshots/migrate", h.SnapshotMigrate)
|
mux.HandleFunc("/api/snapshots/migrate", h.SnapshotMigrate)
|
||||||
mux.HandleFunc("/api/snapshots/regenerate-hourly-reports", h.SnapshotRegenerateHourlyReports)
|
mux.HandleFunc("/api/snapshots/regenerate-hourly-reports", h.SnapshotRegenerateHourlyReports)
|
||||||
|
mux.HandleFunc("/vcenters", h.VcenterList)
|
||||||
|
mux.HandleFunc("/vcenters/totals", h.VcenterTotals)
|
||||||
mux.HandleFunc("/metrics", h.Metrics)
|
mux.HandleFunc("/metrics", h.Metrics)
|
||||||
|
|
||||||
mux.HandleFunc("/snapshots/hourly", h.SnapshotHourlyList)
|
mux.HandleFunc("/snapshots/hourly", h.SnapshotHourlyList)
|
||||||
|
|||||||
Reference in New Issue
Block a user