From 98e92a826435fea6ac954b33791f8326d239f011 Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Fri, 17 Apr 2026 15:21:27 +1000 Subject: [PATCH] updated UI --- components/core/footer.templ | 12 +- components/core/footer_templ.go | 4 +- components/core/header.templ | 4 +- components/core/header_templ.go | 8 +- components/core/ui.templ | 53 +++ components/views/index.templ | 118 +++--- components/views/index_templ.go | 20 +- components/views/snapshots.templ | 132 ++++--- components/views/snapshots_templ.go | 366 +++++++----------- components/views/vm_trace.templ | 166 ++++---- components/views/vm_trace_templ.go | 570 ++++++++++++---------------- design.md | 89 +++++ dist/assets/css/web3.css | 549 ++++++++++++++++++++++----- plan.md | 330 ++++++++++++++++ 14 files changed, 1566 insertions(+), 855 deletions(-) create mode 100644 components/core/ui.templ create mode 100644 design.md create mode 100644 plan.md diff --git a/components/core/footer.templ b/components/core/footer.templ index 4906e0e..5046a1e 100644 --- a/components/core/footer.templ +++ b/components/core/footer.templ @@ -1,9 +1,9 @@ package core templ Footer() { - -} \ No newline at end of file + +} diff --git a/components/core/footer_templ.go b/components/core/footer_templ.go index e973e82..bfc0d62 100644 --- a/components/core/footer_templ.go +++ b/components/core/footer_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.977 +// templ: version: v0.3.1001 package core //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -29,7 +29,7 @@ func Footer() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/components/core/header.templ b/components/core/header.templ index 5df5df7..95c282d 100644 --- a/components/core/header.templ +++ b/components/core/header.templ @@ -6,7 +6,9 @@ templ Header() { - + + + vCTP API diff --git a/components/core/header_templ.go b/components/core/header_templ.go index 3eaae09..e06b930 100644 --- a/components/core/header_templ.go +++ b/components/core/header_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.977 +// templ: version: v0.3.1001 package core //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -31,14 +31,14 @@ func Header() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "vCTP API } -
+
diff --git a/components/views/snapshots_templ.go b/components/views/snapshots_templ.go index fb88a08..0ee34b5 100644 --- a/components/views/snapshots_templ.go +++ b/components/views/snapshots_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.977 +// templ: version: v0.3.1001 package views //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -8,7 +8,10 @@ package views import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -import "vctp/components/core" +import ( + "fmt" + "vctp/components/core" +) type SnapshotEntry struct { Label string @@ -159,114 +162,102 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Snapshot Library

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 60, Col: 50} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + templ_7745c5c3_Err = core.PageHeader( + "Snapshot Library", + title, + subtitle, + []core.ActionLink{ + { + Label: "Back to Dashboard", + Href: "/", + Class: "web2-button secondary", + }, + }, + ).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 61, Col: 56} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + templ_7745c5c3_Err = core.SectionHead("Available Exports", fmt.Sprintf("%d files", len(entries))).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

Back to Dashboard

Available Exports

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 69, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " files
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
SnapshotRecordsDownload
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for i, entry := range entries { if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">Download XLSX") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
SnapshotRecordsDownload
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group) + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 84, Col: 77} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 88, Col: 62} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label) + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 90, Col: 76} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 94, Col: 61} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count) + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 94, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 98, Col: 49} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " recordsDownload XLSX
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -274,7 +265,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -298,12 +289,12 @@ func VcenterList(links []VcenterLink) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var12 := templ.GetChildren(ctx) - if templ_7745c5c3_Var12 == nil { - templ_7745c5c3_Var12 = templ.NopComponent + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -311,34 +302,48 @@ func VcenterList(links []VcenterLink) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
vCenter Inventory

Monitored vCenters

Select a vCenter to view snapshot totals over time.

Back to Dashboard

vCenters

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") 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: 130, Col: 43} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + templ_7745c5c3_Err = core.PageHeader( + "vCenter Inventory", + "Monitored vCenters", + "Select a vCenter to view snapshot totals over time.", + []core.ActionLink{ + { + Label: "Back to Dashboard", + Href: "/", + Class: "web2-button secondary", + }, + }, + ).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " total
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = core.SectionHead("vCenters", fmt.Sprintf("%d total", len(links))).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
vCenterTotals
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, link := range links { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
vCenterTotals
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name) + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 143, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 148, Col: 47} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -346,12 +351,12 @@ func VcenterList(links []VcenterLink) templ.Component { 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) + var templ_7745c5c3_Var11 templ.SafeURL + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 145, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 150, Col: 48} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -392,9 +397,9 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var16 := templ.GetChildren(ctx) - if templ_7745c5c3_Var16 == nil { - templ_7745c5c3_Var16 = templ.NopComponent + 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, 22, "") @@ -405,214 +410,137 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
vCenter Totals

Totals for ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") 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: 169, Col: 63} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + templ_7745c5c3_Err = core.PageHeader( + "vCenter Totals", + "Totals for "+vcenter, + meta.TypeLabel+" snapshots of VM count, vCPU, and RAM over time.", + []core.ActionLink{ + { + Label: "All vCenters", + Href: "/vcenters", + Class: "web2-button secondary", + }, + { + Label: "Dashboard", + Href: "/", + Class: "web2-button", + }, + }, + ).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

") + templ_7745c5c3_Err = core.SegmentedActions( + []core.SegmentedLink{ + { + Label: "Hourly Detail (45d)", + Href: meta.HourlyLink, + Class: meta.HourlyClass, + }, + { + Label: "Daily Aggregated", + Href: meta.DailyLink, + Class: meta.DailyClass, + }, + }, + ).Render(ctx, templ_7745c5c3_Buffer) 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: 170, Col: 62} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

") 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.

") - 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, "Hourly Detail (45d) ") - 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, "Daily Aggregated

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 184, Col: 56} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " Snapshots

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 185, Col: 45} - } - _, 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, " records
") + templ_7745c5c3_Err = core.SectionHead(meta.TypeLabel+" Snapshots", fmt.Sprintf("%d records", len(entries))).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if chart.ConfigJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
Snapshot TimeVMsvCPUsRAM (GB)
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, entry := range entries { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
Snapshot TimeVMsvCPUsRAM (GB)
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot) + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 214, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 232, Col: 30} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) + _, 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, 38, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount) + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 215, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 233, Col: 48} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + _, 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, 39, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 216, Col: 50} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 234, Col: 50} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 235, Col: 51} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) + _, 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, 41, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -620,7 +548,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/components/views/vm_trace.templ b/components/views/vm_trace.templ index eaf1f33..8791e6f 100644 --- a/components/views/vm_trace.templ +++ b/components/views/vm_trace.templ @@ -6,18 +6,18 @@ import ( ) type VmTraceEntry struct { - Snapshot string - RawTime int64 - Name string - VmId string - VmUuid string - Vcenter string - ResourcePool string - VcpuCount int64 - RamGB int64 + Snapshot string + RawTime int64 + Name string + VmId string + VmUuid string + Vcenter string + ResourcePool string + VcpuCount int64 + RamGB int64 ProvisionedDisk float64 - CreationTime string - DeletionTime string + CreationTime string + DeletionTime string } type VmTraceChart struct { @@ -25,12 +25,12 @@ type VmTraceChart struct { } type VmTraceMeta struct { - ViewType string - TypeLabel string - HourlyLink string - DailyLink string + ViewType string + TypeLabel string + HourlyLink string + DailyLink string HourlyClass string - DailyClass string + DailyClass string } type VmTraceDiagnosticLine struct { @@ -40,7 +40,7 @@ type VmTraceDiagnosticLine struct { type VmTraceDiagnostics struct { Visible bool - Lines []VmTraceDiagnosticLine + Lines []VmTraceDiagnosticLine } templ VmTracePage(query string, display_query string, vm_id string, vm_uuid string, vm_name string, creationLabel string, deletionLabel string, creationApprox bool, entries []VmTraceEntry, chart VmTraceChart, meta VmTraceMeta, diagnostics VmTraceDiagnostics) { @@ -48,52 +48,60 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri @core.Header() -
-
-
-
-
VM Trace
-

Snapshot history{display_query}

-

Timeline of vCPU, RAM, and resource pool changes across { meta.TypeLabel } snapshots.

-
-
- Dashboard -
+
+
+ @core.PageHeader( + "VM Trace", + "Snapshot history"+display_query, + "Timeline of vCPU, RAM, and resource pool changes across "+meta.TypeLabel+" snapshots.", + []core.ActionLink{ + { + Label: "Dashboard", + Href: "/", + Class: "web2-button secondary", + }, + }, + ) +
+ +
+ +
- - -
- - -
-
- - -
-
- - -
-
- - Clear -
-
-
- Hourly Detail - Daily Aggregated +
+ +
-
- +
+ + +
+
+ + Clear +
+ + @core.SegmentedActions( + []core.SegmentedLink{ + { + Label: "Hourly Detail", + Href: meta.HourlyLink, + Class: meta.HourlyClass, + }, + { + Label: "Daily Aggregated", + Href: meta.DailyLink, + Class: meta.DailyClass, + }, + }, + ) +
-
-

{ meta.TypeLabel } Timeline

- {len(entries)} samples -
+ @core.SectionHead(meta.TypeLabel+" Timeline", fmt.Sprintf("%d samples", len(entries))) if chart.ConfigJSON != "" {
- +
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

Creation Time

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(vm_name) + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(creationLabel) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 75, Col: 123} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 118, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" placeholder=\"VM name\">

Clear
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 = []any{meta.HourlyClass} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) + if creationApprox { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

Approximate (earliest snapshot)

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

Deletion Time

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" href=\"") + if diagnostics.Visible && len(diagnostics.Lines) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
Lifecycle diagnostics
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, line := range diagnostics.Lines { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(line.Label) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 136, Col: 55} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(line.Value) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 137, Col: 47} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 templ.SafeURL - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(meta.HourlyLink) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 83, Col: 59} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">Hourly Detail ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var11 = []any{meta.DailyClass} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Daily Aggregated

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 90, Col: 56} - } - _, 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, 15, " Timeline

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 91, Col: 44} - } - _, 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, 16, " samples
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if chart.ConfigJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Creation time

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(creationLabel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 110, Col: 76} - } - _, 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, 20, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if creationApprox { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

Approximate (earliest snapshot)

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

Deletion time

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(deletionLabel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 117, Col: 76} - } - _, 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, 23, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if diagnostics.Visible && len(diagnostics.Lines) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
Lifecycle diagnostics
SnapshotVM NameVmIdVmUuidVcenterResource PoolvCPUsRAM (GB)Disk
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(e.Snapshot) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 163, Col: 26} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 164, Col: 22} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmId) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 165, Col: 22} + } + _, 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, 25, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmUuid) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 166, Col: 24} + } + _, 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, 26, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vcenter) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 167, Col: 25} + } + _, 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, 27, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON) + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(e.ResourcePool) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 96, Col: 133} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 168, Col: 30} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(e.VcpuCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 169, Col: 46} + } + _, 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, 29, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(e.RamGB) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 170, Col: 42} + } + _, 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, 30, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", e.ProvisionedDisk)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 171, Col: 73} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, line := range diagnostics.Lines { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(line.Label) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 128, Col: 70} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(line.Value) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 129, Col: 51} - } - _, 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, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, e := range entries { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
SnapshotVM NameVmIdVmUuidVcenterResource PoolvCPUsRAM (GB)Disk
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(e.Snapshot) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 155, Col: 25} - } - _, 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, 31, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 156, Col: 21} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmId) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 157, Col: 21} - } - _, 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, 33, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmUuid) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 158, Col: 23} - } - _, 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, 34, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vcenter) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 159, Col: 24} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(e.ResourcePool) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 160, Col: 29} - } - _, 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, 36, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(e.VcpuCount) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 161, Col: 45} - } - _, 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, 37, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(e.RamGB) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 162, Col: 41} - } - _, 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, 38, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", e.ProvisionedDisk)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 163, Col: 72} - } - _, 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, 39, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -494,7 +416,7 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/design.md b/design.md new file mode 100644 index 0000000..fe96fa3 --- /dev/null +++ b/design.md @@ -0,0 +1,89 @@ +# Design System Inspired by Airtable + +## 1. Visual Theme & Atmosphere + +Airtable's website is a clean, enterprise-friendly platform that communicates "sophisticated simplicity" through a white canvas with deep navy text (`#181d26`) and Airtable Blue (`#1b61c9`) as the primary interactive accent. The Haas font family (display + text variants) creates a Swiss-precision typography system with positive letter-spacing throughout. + +**Key Characteristics:** +- White canvas with deep navy text (`#181d26`) +- Airtable Blue (`#1b61c9`) as primary CTA and link color +- Haas + Haas Groot Disp dual font system +- Positive letter-spacing on body text (0.08px–0.28px) +- 12px radius buttons, 16px–32px for cards +- Multi-layer blue-tinted shadow: `rgba(45,127,249,0.28) 0px 1px 3px` +- Semantic theme tokens: `--theme_*` CSS variable naming + +## 2. Color Palette & Roles + +### Primary +- **Deep Navy** (`#181d26`): Primary text +- **Airtable Blue** (`#1b61c9`): CTA buttons, links +- **White** (`#ffffff`): Primary surface +- **Spotlight** (`rgba(249,252,255,0.97)`): `--theme_button-text-spotlight` + +### Semantic +- **Success Green** (`#006400`): `--theme_success-text` +- **Weak Text** (`rgba(4,14,32,0.69)`): `--theme_text-weak` +- **Secondary Active** (`rgba(7,12,20,0.82)`): `--theme_button-text-secondary-active` + +### Neutral +- **Dark Gray** (`#333333`): Secondary text +- **Mid Blue** (`#254fad`): Link/accent blue variant +- **Border** (`#e0e2e6`): Card borders +- **Light Surface** (`#f8fafc`): Subtle surface + +### Shadows +- **Blue-tinted** (`rgba(0,0,0,0.32) 0px 0px 1px, rgba(0,0,0,0.08) 0px 0px 2px, rgba(45,127,249,0.28) 0px 1px 3px, rgba(0,0,0,0.06) 0px 0px 0px 0.5px inset`) +- **Soft** (`rgba(15,48,106,0.05) 0px 0px 20px`) + +## 3. Typography Rules + +### Font Families +- **Primary**: `Haas`, fallbacks: `-apple-system, system-ui, Segoe UI, Roboto` +- **Display**: `Haas Groot Disp`, fallback: `Haas` + +### Hierarchy + +| Role | Font | Size | Weight | Line Height | Letter Spacing | +|------|------|------|--------|-------------|----------------| +| Display Hero | Haas | 48px | 400 | 1.15 | normal | +| Display Bold | Haas Groot Disp | 48px | 900 | 1.50 | normal | +| Section Heading | Haas | 40px | 400 | 1.25 | normal | +| Sub-heading | Haas | 32px | 400–500 | 1.15–1.25 | normal | +| Card Title | Haas | 24px | 400 | 1.20–1.30 | 0.12px | +| Feature | Haas | 20px | 400 | 1.25–1.50 | 0.1px | +| Body | Haas | 18px | 400 | 1.35 | 0.18px | +| Body Medium | Haas | 16px | 500 | 1.30 | 0.08–0.16px | +| Button | Haas | 16px | 500 | 1.25–1.30 | 0.08px | +| Caption | Haas | 14px | 400–500 | 1.25–1.35 | 0.07–0.28px | + +## 4. Component Stylings + +### Buttons +- **Primary Blue**: `#1b61c9`, white text, 16px 24px padding, 12px radius +- **White**: white bg, `#181d26` text, 12px radius, 1px border white +- **Cookie Consent**: `#1b61c9` bg, 2px radius (sharp) + +### Cards: `1px solid #e0e2e6`, 16px–24px radius +### Inputs: Standard Haas styling + +## 5. Layout +- Spacing: 1–48px (8px base) +- Radius: 2px (small), 12px (buttons), 16px (cards), 24px (sections), 32px (large), 50% (circles) + +## 6. Depth +- Blue-tinted multi-layer shadow system +- Soft ambient: `rgba(15,48,106,0.05) 0px 0px 20px` + +## 7. Do's and Don'ts +### Do: Use Airtable Blue for CTAs, Haas with positive tracking, 12px radius buttons +### Don't: Skip positive letter-spacing, use heavy shadows + +## 8. Responsive Behavior +Breakpoints: 425–1664px (23 breakpoints) + +## 9. Agent Prompt Guide +- Text: Deep Navy (`#181d26`) +- CTA: Airtable Blue (`#1b61c9`) +- Background: White (`#ffffff`) +- Border: `#e0e2e6` diff --git a/dist/assets/css/web3.css b/dist/assets/css/web3.css index 147a79d..c8a1987 100644 --- a/dist/assets/css/web3.css +++ b/dist/assets/css/web3.css @@ -1,45 +1,182 @@ :root { - --web2-blue: #1d9bf0; - --web2-slate: #0f172a; - --web2-muted: #64748b; - --web2-card: #ffffff; - --web2-border: #e5e7eb; + --theme_text_primary: #181d26; + --theme_text_weak: rgba(4, 14, 32, 0.69); + --theme_text_inverse: rgba(249, 252, 255, 0.97); + --theme_text_secondary_active: rgba(7, 12, 20, 0.82); + --theme_accent_blue: #1b61c9; + --theme_accent_blue_hover: #164fa6; + --theme_accent_blue_soft: #e8f0fc; + --theme_success_text: #006400; + --theme_surface_primary: #ffffff; + --theme_surface_subtle: #f8fafc; + --theme_surface_shell: #fdfefe; + --theme_surface_section: #fbfdff; + --theme_border: #e0e2e6; + --theme_shadow_card: rgba(0, 0, 0, 0.32) 0 0 1px, rgba(0, 0, 0, 0.08) 0 0 2px, rgba(45, 127, 249, 0.28) 0 1px 3px, rgba(0, 0, 0, 0.06) 0 0 0 0.5px inset; + --theme_shadow_soft: rgba(15, 48, 106, 0.05) 0 0 20px; + --theme_shadow_button: rgba(45, 127, 249, 0.28) 0 1px 3px; + --theme_radius_button: 12px; + --theme_radius_card: 16px; + --theme_radius_section: 24px; + --theme_radius_large: 32px; + --theme_font_body: "Haas", "Neue Haas Grotesk Text Pro", "Avenir Next", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --theme_font_display: "Haas Groot Disp", "Haas", "Neue Haas Grotesk Display Pro", "Avenir Next", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --theme_font_code: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --theme_letter_body: 0.12px; + --theme_letter_caption: 0.2px; + --theme_letter_button: 0.08px; + --theme_transition_fast: 150ms ease; + --theme_transition_base: 220ms ease; + --web2-blue: var(--theme_accent_blue); + --web2-slate: var(--theme_text_primary); + --web2-muted: var(--theme_text_weak); + --web2-card: var(--theme_surface_primary); + --web2-border: var(--theme_border); } + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, body { - font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; - color: var(--web2-slate); + margin: 0; + padding: 0; } + +body { + font-family: var(--theme_font_body); + color: var(--theme_text_primary); + letter-spacing: var(--theme_letter_body); + background: var(--theme_surface_shell); +} + .web2-bg { - background: #ffffff; + background: + radial-gradient(circle at 8% 4%, rgba(27, 97, 201, 0.08) 0, rgba(27, 97, 201, 0) 30%), + radial-gradient(circle at 90% 12%, rgba(37, 79, 173, 0.08) 0, rgba(37, 79, 173, 0) 28%), + var(--theme_surface_shell); } + .web2-shell { - max-width: 1100px; + max-width: 1140px; margin: 0 auto; - padding: 2rem 1.5rem 4rem; + padding: 2.5rem 1.5rem 2.25rem; } + .web2-shell-wide { - max-width: 1400px; + max-width: 1420px; } + +.web2-page-head { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.web2-page-head-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.web2-head-copy { + max-width: 72ch; +} + +.web2-page-title { + margin-top: 0.6rem; + font-family: var(--theme_font_display); + font-size: clamp(1.95rem, 1.2rem + 1.9vw, 2.65rem); + line-height: 1.15; + letter-spacing: 0.06px; +} + +.web2-page-subtitle { + margin-top: 0.45rem; + font-size: 0.96rem; + line-height: 1.45; + color: var(--theme_text_weak); +} + +.web2-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +.web2-section-head { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.web2-kpi-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.web2-kpi-label { + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.22em; + color: var(--theme_text_weak); +} + +.web2-kpi-value { + margin-top: 0.55rem; + font-size: 1.3rem; + font-weight: 600; + line-height: 1.2; + color: var(--theme_text_primary); +} + +.web2-note { + padding: 0.78rem 0.92rem; + border-radius: var(--theme_radius_button); + border: 1px solid rgba(27, 97, 201, 0.28); + background: rgba(27, 97, 201, 0.08); + color: var(--theme_text_secondary_active); + font-size: 0.89rem; + line-height: 1.45; +} + .web2-header { - background: var(--web2-card); - border: 1px solid var(--web2-border); - border-radius: 4px; - padding: 1.5rem 2rem; + background: var(--theme_surface_section); + border: 1px solid var(--theme_border); + border-radius: var(--theme_radius_section); + padding: 1.65rem 2rem; + box-shadow: var(--theme_shadow_card), var(--theme_shadow_soft); } + .web2-card { - background: var(--web2-card); - border: 1px solid var(--web2-border); - border-radius: 4px; - padding: 1.5rem 1.75rem; + background: var(--theme_surface_primary); + border: 1px solid var(--theme_border); + border-radius: var(--theme_radius_card); + padding: 1.4rem 1.6rem; + box-shadow: var(--theme_shadow_card); } + .web2-card h2 { position: relative; - padding-left: 0.75rem; - font-size: 1.05rem; - font-weight: 700; - letter-spacing: 0.02em; - color: #0b1220; + padding-left: 0.9rem; + font-size: 1.1rem; + font-family: var(--theme_font_display); + font-weight: 600; + letter-spacing: var(--theme_letter_button); + color: var(--theme_text_primary); } + .web2-card h2::before { content: ""; position: absolute; @@ -47,191 +184,355 @@ body { top: 50%; transform: translateY(-50%); width: 4px; - height: 70%; - background: var(--web2-blue); - border-radius: 2px; - box-shadow: 0 0 0 1px rgba(29, 155, 240, 0.18); + height: 72%; + background: var(--theme_accent_blue); + border-radius: 999px; + box-shadow: 0 0 0 1px rgba(27, 97, 201, 0.22); } + .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; + background: var(--theme_surface_subtle); + border: 1px solid var(--theme_border); + color: var(--theme_text_weak); + padding: 0.28rem 0.72rem; + border-radius: var(--theme_radius_button); + font-size: 0.82rem; + font-weight: 500; + letter-spacing: 0.24px; } + .web2-code { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - background: #f1f5f9; - border: 1px solid var(--web2-border); - border-radius: 3px; - padding: 0.1rem 0.35rem; - font-size: 0.85em; - color: #0f172a; + font-family: var(--theme_font_code); + background: rgba(27, 97, 201, 0.07); + border: 1px solid var(--theme_border); + border-radius: 8px; + padding: 0.12rem 0.42rem; + font-size: 0.84em; + color: var(--theme_text_primary); } + .web2-paragraphs p + p { margin-top: 0.85rem; } + .web2-link { - color: var(--web2-blue); + color: var(--theme_accent_blue); text-decoration: none; font-weight: 600; + transition: color var(--theme_transition_fast), text-decoration-color var(--theme_transition_fast); } + .web2-link:hover { + color: var(--theme_accent_blue_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; + background: var(--theme_accent_blue); + color: var(--theme_text_inverse); + padding: 0.56rem 1rem; + border-radius: var(--theme_radius_button); + border: 1px solid rgba(22, 79, 166, 0.95); + box-shadow: var(--theme_shadow_button); font-weight: 600; + letter-spacing: var(--theme_letter_button); text-decoration: none; + transition: transform var(--theme_transition_fast), background var(--theme_transition_fast), border-color var(--theme_transition_fast), box-shadow var(--theme_transition_fast); } + .web2-button:hover { - background: #1787d4; + background: var(--theme_accent_blue_hover); + border-color: var(--theme_accent_blue_hover); + box-shadow: rgba(37, 79, 173, 0.32) 0 3px 7px; + transform: translateY(-1px); } + +.web2-button.secondary { + background: var(--theme_surface_primary); + color: var(--theme_text_secondary_active); + border-color: var(--theme_border); + box-shadow: none; +} + +.web2-button.secondary:hover { + background: var(--theme_surface_subtle); + border-color: #c7cdd6; + color: var(--theme_text_primary); + transform: none; +} + .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; + background: var(--theme_surface_subtle); + color: var(--theme_text_primary); + padding: 0.52rem 1.02rem; + border-radius: var(--theme_radius_button); + border: 1px solid var(--theme_border); 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; + letter-spacing: var(--theme_letter_button); + transition: background var(--theme_transition_fast), border-color var(--theme_transition_fast), color var(--theme_transition_fast), box-shadow var(--theme_transition_fast); display: inline-flex; align-items: center; gap: 0.35rem; } + .web3-button:hover { - background: #e2e8f0; - border-color: #cbd5e1; + background: #edf2fa; + border-color: #c8d2e1; } + .web3-button.active { - background: #dbeafe; - border-color: #93c5fd; - color: #1d4ed8; - box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.35); + background: var(--theme_accent_blue_soft); + border-color: rgba(27, 97, 201, 0.45); + color: var(--theme_accent_blue); + box-shadow: rgba(45, 127, 249, 0.28) 0 0 0 2px; } + .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-shell { + overflow-x: auto; + overflow-y: hidden; + border: 1px solid var(--theme_border); + border-radius: var(--theme_radius_card); + background: var(--theme_surface_primary); } + +.web2-list li { + background: var(--theme_surface_primary); + border: 1px solid var(--theme_border); + border-radius: var(--theme_radius_card); + padding: 0.75rem 1rem; + box-shadow: var(--theme_shadow_card); +} + .web2-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; + letter-spacing: 0.08px; + min-width: 560px; } + .web2-table thead th { text-align: left; - padding: 0.75rem 0.5rem; + padding: 0.8rem 0.55rem; font-weight: 700; - color: var(--web2-muted); - border-bottom: 1px solid var(--web2-border); + color: var(--theme_text_weak); + letter-spacing: var(--theme_letter_caption); + border-bottom: 1px solid var(--theme_border); + background: rgba(248, 250, 252, 0.95); } + .web2-table tbody td { - padding: 0.9rem 0.5rem; - border-bottom: 1px solid var(--web2-border); + padding: 0.92rem 0.55rem; + border-bottom: 1px solid var(--theme_border); } + .web2-table tbody tr:nth-child(odd) { - background: #f8fafc; + background: var(--theme_surface_subtle); } + .web2-table tbody tr:nth-child(even) { - background: #ffffff; + background: var(--theme_surface_primary); } + .web2-group-row td { - background: #e8eef5; - color: #0f172a; - border-bottom: 1px solid var(--web2-border); - padding: 0.65rem 0.5rem; + background: #eaf1fb; + color: var(--theme_text_primary); + border-bottom: 1px solid var(--theme_border); + padding: 0.7rem 0.55rem; } + .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; + border: 1px solid var(--theme_border); + padding: 0.16rem 0.5rem; + border-radius: var(--theme_radius_button); font-size: 0.8rem; - color: var(--web2-muted); - background: #f8fafc; + letter-spacing: var(--theme_letter_caption); + color: var(--theme_text_weak); + background: var(--theme_surface_subtle); } + +.web2-form-grid { + display: grid; + gap: 0.75rem; + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.web2-field { + display: flex; + flex-direction: column; + gap: 0.32rem; +} + +.web2-label { + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.1px; + color: var(--theme_text_weak); +} + +.web2-input { + width: 100%; + padding: 0.62rem 0.72rem; + border: 1px solid var(--theme_border); + border-radius: var(--theme_radius_button); + background: var(--theme_surface_primary); + color: var(--theme_text_primary); + font: inherit; + letter-spacing: var(--theme_letter_body); + transition: border-color var(--theme_transition_fast), box-shadow var(--theme_transition_fast), background var(--theme_transition_fast); +} + +.web2-input::placeholder { + color: rgba(4, 14, 32, 0.46); +} + +.web2-input:hover { + border-color: #c9d0db; +} + +.web2-input:focus-visible { + outline: none; + border-color: rgba(27, 97, 201, 0.8); + box-shadow: rgba(45, 127, 249, 0.26) 0 0 0 3px; +} + +.web2-form-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.web2-form-actions-full { + grid-column: 1 / -1; +} + +.web2-subcard { + padding: 0.95rem 1rem; + border-radius: var(--theme_radius_card); + border: 1px solid var(--theme_border); + background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0, rgba(248, 250, 252, 0.75) 100%); +} + +.web2-subcard-label { + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.2em; + color: var(--theme_text_weak); +} + +.web2-subcard-value { + margin-top: 0.45rem; + font-size: 1rem; + font-weight: 600; + color: var(--theme_text_primary); +} + +.web2-muted { + color: var(--theme_text_weak); +} + +.web2-caption { + font-size: 0.76rem; + letter-spacing: var(--theme_letter_caption); +} + +.web2-details-summary { + cursor: pointer; + font-size: 0.86rem; + font-weight: 600; + color: var(--theme_text_secondary_active); +} + +.web2-card-grid { + display: grid; + gap: 1.4rem; +} + .web3-chart-frame { position: relative; min-width: 760px; width: 100%; } + .web3-chart-canvas { display: block; width: 100%; height: 360px; - background: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 6px; + background: var(--theme_surface_primary); + border: 1px solid var(--theme_border); + border-radius: var(--theme_radius_card); } + .web3-chart-tooltip { position: absolute; left: 0; top: 0; opacity: 0; pointer-events: none; - background: rgba(15, 23, 42, 0.95); - color: #f8fafc; + background: rgba(24, 29, 38, 0.97); + color: var(--theme_text_inverse); padding: 0.55rem 0.65rem; - border-radius: 6px; + border-radius: var(--theme_radius_button); font-size: 0.75rem; line-height: 1.35; min-width: 170px; box-shadow: 0 10px 30px rgba(2, 6, 23, 0.25); z-index: 20; - transition: opacity 0.08s linear; + transition: opacity 80ms linear; } + .web3-chart-tooltip.visible { opacity: 1; } + .web3-chart-tooltip-title { font-weight: 700; color: #e2e8f0; margin-bottom: 0.35rem; } + .web3-chart-tooltip-row { display: flex; align-items: center; justify-content: space-between; gap: 0.65rem; } + .web3-chart-tooltip-label { display: inline-flex; align-items: center; color: #cbd5e1; } + .web3-chart-tooltip-value { font-weight: 700; color: #f8fafc; } + .web3-chart-tooltip-swatch { display: inline-block; width: 8px; @@ -239,8 +540,74 @@ body { border-radius: 999px; margin-right: 0.35rem; } + +.web2-footer { + width: 100%; + padding: 0.65rem 1.5rem 1.25rem; +} + +.web2-footer-inner { + max-width: 1140px; + margin: 0 auto; + border-top: 1px solid var(--theme_border); + padding-top: 0.75rem; + text-align: center; + font-size: 0.74rem; + font-style: italic; + letter-spacing: var(--theme_letter_caption); + color: var(--theme_text_weak); +} + +a:focus-visible, +button:focus-visible, +summary:focus-visible, +.web2-button:focus-visible, +.web3-button:focus-visible, +.web2-link:focus-visible { + outline: 2px solid rgba(27, 97, 201, 0.7); + outline-offset: 2px; +} + @media (max-width: 900px) { + .web2-shell { + padding: 1.5rem 1rem 1.25rem; + } + .web2-header { + border-radius: var(--theme_radius_card); + padding: 1.2rem 1rem; + } + .web2-card { + padding: 1.1rem 1rem; + } .web3-chart-frame { min-width: 640px; } + .web2-footer { + padding: 0.5rem 1rem 1rem; + } + .web2-page-title { + font-size: 1.82rem; + } + .web2-page-subtitle { + font-size: 0.9rem; + } + .web2-actions { + width: 100%; + } + .web2-actions .web2-button { + flex: 1 1 auto; + text-align: center; + } + .web2-table { + min-width: 520px; + } +} + +@media (min-width: 780px) { + .web2-kpi-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .web2-form-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } } diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..d22d122 --- /dev/null +++ b/plan.md @@ -0,0 +1,330 @@ +# Inventory Capture and Aggregation Optimization Plan + +## Summary +Optimize for end-to-end runtime with a Postgres-ready design. Keep the current HTTP and report behavior intact, but shift the scheduled data pipeline so it uses canonical append-only/cache tables instead of repeatedly scanning `inventory_hourly_*` tables and regenerating reports inline. + +This plan is intended to be implementation-ready for a `codex-5.3` execution pass. + +Execution-path decision: +- For the current architecture and migration phases, scheduled daily and monthly aggregation default to the Go path. +- This is a readability-first and current-performance decision, not a claim that Go is inherently faster than a well-designed SQL implementation. +- SQL path is retained for compatibility, backfill, and fallback. +- SQL remains a future optimization candidate on canonical Postgres tables. +- SQL can be promoted to default only after benchmark evidence on canonical Postgres tables shows a clear runtime advantage. + +The target architecture is: +1. `vm_hourly_stats` is the canonical hourly fact store. +2. `vm_daily_rollup` is the canonical monthly input. +3. Per-snapshot tables and XLSX generation remain as compatibility and output concerns, not the primary execution path. + +## Current State +- Hourly capture already writes both per-snapshot tables and `vm_hourly_stats`. +- Daily aggregation has mixed execution paths: + - SQL union path over `inventory_hourly_*` + - Go path over `vm_hourly_stats` or parallel table scans +- Monthly aggregation has mixed execution paths: + - SQL path over daily or hourly snapshot tables + - Go path over `vm_daily_rollup` or hourly cache +- Lifecycle reconciliation updates both canonical cache tables and prior hourly snapshot tables during the hot path. +- Report generation is still coupled to scheduled capture and aggregation jobs. +- The current UI is rendered through Templ pages and shared `web2`/`web3` CSS classes, but it does not yet match the visual system described in `design.md`. +- Current shipped styling still uses a different blue accent, tighter radii, default system typography, and inconsistent component hierarchy compared with the target design language. + +## Implementation Goals +- Reduce hourly capture wall-clock time. +- Reduce daily and monthly aggregation runtime. +- Eliminate repeated historical table scans from the normal scheduled path. +- Keep user-visible HTTP APIs, reports, and auth behavior unchanged. +- Improve UI clarity and consistency so the dashboard, snapshot views, and trace views reflect the design direction in `design.md`. +- Make authentication and role requirements easier to understand from the UI without changing the auth model. +- Preserve compatibility with SQLite for development and small installs. +- Make the runtime architecture cleanly scalable for PostgreSQL production use. + +## Implementation Changes + +### 1. Hourly Capture Pipeline +- Keep `GetAllVMsWithProps` as the primary vCenter inventory fetch path. +- Preserve single-VM property retrieval only as a fallback path when bulk retrieval is incomplete. +- Replace row-by-row database writes in hourly capture with batched writes. +- For PostgreSQL: + - prefer multi-row insert/upsert or `COPY` into `vm_hourly_stats` + - keep conflict handling on the canonical key +- For SQLite: + - keep transactional batched insert/upsert + - do not attempt PostgreSQL-only ingestion patterns +- During capture, write data to these canonical destinations first: + - `vm_hourly_stats` + - `vm_lifecycle_cache` + - `vcenter_totals` + - `vcenter_latest_totals` + - `vcenter_aggregate_totals` for hourly totals +- Treat `inventory_hourly_` as compatibility output, not as the source of truth for downstream jobs. +- Move deletion and event reconciliation to one post-capture reconciliation phase per vCenter. +- In that reconciliation phase, update canonical cache tables first. +- Stop updating prior hourly snapshot tables inline during the capture hot path except where compatibility mode explicitly requires it. +- Remove synchronous XLSX regeneration from hourly capture. +- Scheduled capture should finish once persistence and reconciliation are complete. +- Report generation should run after the capture path, either deferred within the job or via a follow-up stage. + +### 2. Daily Aggregation +- Make `vm_hourly_stats` the only normal scheduled input for daily aggregation. +- Scheduled daily jobs must not build `UNION ALL` queries across `inventory_hourly_*`. +- Keep the Go aggregation path as the explicit default scheduled path for the current implementation and migration phases. +- Readability is the primary reason for this default: the Go path is materially easier to follow, test, and debug than the current snapshot-union SQL path. +- Performance is a secondary but still important reason: on the current implementation, Go is expected to outperform the existing SQL union path by avoiding repeated historical table scans. +- Treat the SQL path as non-default compatibility and fallback behavior. +- Do not treat this as a permanent rejection of SQL. +- Only promote SQL to default if benchmark results on canonical Postgres data show a clear, repeatable improvement over the Go path. +- Keep the current SQL union path only for: + - compatibility fallback + - manual repair + - backfill support where needed +- Daily aggregation output must continue writing: + - `inventory_daily_summary_YYYYMMDD` + - `vm_daily_rollup` + - `snapshot_registry` daily record + - refreshed `vcenter_aggregate_totals` daily entries +- Lifecycle refinement should operate on canonical lifecycle data and only use snapshot-table probing as fallback. +- Preserve existing daily semantics for: + - `SamplesPresent` + - `AvgIsPresent` + - weighted CPU/RAM/disk averages + - pool percentages + - creation/deletion time behavior + +### 3. Monthly Aggregation +- Make `vm_daily_rollup` the default scheduled input for monthly aggregation. +- Scheduled monthly jobs should not scan hourly snapshot tables in the normal path. +- Keep the Go aggregation path as the explicit default scheduled path for the current implementation and migration phases. +- Readability is the primary reason for this default: the Go path is materially easier to follow, test, and debug than the current SQL path. +- Performance is a secondary but still important reason: on the current implementation, Go is expected to outperform the existing SQL path by avoiding snapshot-table unions and hourly-history scans in the normal case. +- Treat the SQL path as non-default compatibility and fallback behavior. +- Do not treat this as a permanent rejection of SQL. +- Only promote SQL to default if benchmark results on canonical Postgres data show a clear, repeatable improvement over the Go path. +- Keep hourly-based monthly aggregation only for: + - manual rebuilds + - repair/backfill workflows + - validation against old behavior +- Preserve current monthly weighting semantics based on per-day sample volumes. +- Monthly aggregation output must continue writing: + - `inventory_monthly_summary_YYYYMM` + - `snapshot_registry` monthly record + - refreshed `vcenter_aggregate_totals` monthly entries +- Keep report generation behavior unchanged from the user’s perspective, but do not keep it on the critical aggregation hot path if it can be deferred safely. + +### 4. Storage and Schema +- Keep these tables during migration: + - `inventory_hourly_*` + - `inventory_daily_summary_*` + - `inventory_monthly_summary_*` +- Stop treating hourly snapshot tables as the normal scheduled aggregation source. +- Preserve `snapshot_registry`, but register logical hourly snapshots by timestamp even when downstream jobs no longer depend on hourly table scans. +- Validate or add the following indexes on `vm_hourly_stats` for PostgreSQL: + - `("SnapshotTime")` + - `("Vcenter","SnapshotTime")` + - `("Vcenter","VmId","SnapshotTime")` + - `("Vcenter","VmUuid","SnapshotTime")` + - a name lookup index aligned with current trace queries +- Keep the existing trace-compatible indexes for SQLite. +- After the canonical-path migration is stable, partition `vm_hourly_stats` by snapshot month for PostgreSQL. +- Do not require partitioning for SQLite or tests. + +### 5. Compatibility Mode +- Introduce an explicit compatibility mode for legacy snapshot tables. +- When compatibility mode is enabled: + - continue writing `inventory_hourly_*` + - continue generating legacy-compatible daily/monthly summary tables + - continue registering snapshots as today +- When compatibility mode is disabled in a later phase: + - scheduled jobs may skip legacy hourly table creation + - compatibility reports and endpoints must still work from canonical data or compatibility rebuild jobs +- Default to compatibility mode enabled during the transition. + +### 6. Scheduling and Job Flow +- Refactor the scheduled pipeline into explicit stages: + 1. capture + 2. reconcile + 3. register and refresh totals caches + 4. optional report generation +- Daily aggregation should run only against the completed prior-day hourly data. +- Monthly aggregation should depend on daily rollup completion, not hourly history scans. +- Keep the current cron behavior and auth/UI behavior unchanged while internal data flow changes land. +- Backfill and repair jobs should rebuild canonical caches first, then compatibility tables and reports. + +### 7. UI Refresh and Design-System Alignment +- Use `design.md` as the source of truth for the UI refresh, but adapt it pragmatically to this codebase rather than attempting a pixel-perfect clone. +- Introduce semantic theme tokens using `--theme_*` naming in the shared stylesheet layer. +- Replace the current ad hoc `web2` color and radius values with tokenized equivalents for: + - primary text + - weak text + - CTA blue + - borders + - surfaces + - success states + - button spotlight text + - card and ambient shadows +- Update the shared stylesheet source and shipped compiled assets so the new tokens flow through the delivered UI. +- Keep the existing `web2` and `web3` class names if that reduces churn, but rebase them on the new token system. +- Establish a typography strategy that follows `design.md` while remaining deployable: + - prefer Haas and Haas Groot Disp only if licensed webfont delivery is available + - otherwise define a documented fallback stack with similar proportions and spacing behavior + - apply positive letter spacing to body, caption, and button treatments where appropriate +- Normalize component shape language to the design brief: + - buttons at 12px radius + - cards and sections at 16px to 24px radius + - larger containers at 24px to 32px radius where needed + - avoid the current 3px to 6px rounded treatment as the default visual language +- Replace the current flat visual treatment with the documented blue-tinted shadow system, but keep shadows controlled and readable in data-heavy views. +- Refactor shared UI structure in the Templ layer: + - `components/core/header.templ` + - `components/core/footer.templ` + - shared shell/header/card/button/table/form patterns used across `components/views/*` +- Add a reusable page-shell pattern so all primary pages share: + - a consistent hero/header treatment + - action grouping + - content width rules + - section spacing + - responsive table overflow behavior +- Improve the dashboard information architecture in `components/views/index.templ`: + - reduce the current long-form text density + - promote primary navigation and key operational tasks + - move build metadata into secondary status cards + - present auth requirements and role policy as a concise callout rather than dense paragraph copy +- Improve snapshot and vCenter list pages in `components/views/snapshots.templ`: + - stronger table hierarchy + - clearer record counts and grouping + - more intentional page headers and return navigation + - responsive behavior that preserves readability on smaller screens +- Improve the VM trace page in `components/views/vm_trace.templ`: + - upgrade search form layout and input styling + - improve chart framing and diagnostics presentation + - make lifecycle summary cards visually clearer + - preserve dense tabular detail without making the page feel purely utilitarian +- Ensure the auth-enabled experience is visible in the UI: + - clarify that UI pages remain public while APIs require Bearer tokens when auth is enabled + - surface viewer versus admin capability differences in concise language + - keep Swagger and operational links accessible from the main navigation +- Add accessibility and interaction requirements to the UI implementation: + - visible focus states + - sufficient text/background contrast + - keyboard-usable navigation and forms + - table layouts that remain readable with horizontal overflow + - mobile-safe spacing and tap targets +- Keep UI changes implementation-friendly: + - avoid introducing a large frontend framework + - continue using Templ plus shared CSS and existing JS assets + - prefer incremental component replacement over a full frontend rewrite + +## Public Interfaces and Settings +- No HTTP API changes are required. +- Keep existing endpoints and report filenames stable. +- No auth-model changes are required for the UI refresh. +- If licensed fonts are not available for deployment, the implementation must ship with a documented fallback stack rather than blocking the UI work. +- Add these settings: + - `settings.capture_write_batch_size` + - default: `1000` + - controls batched DB writes for hourly capture + - `settings.snapshot_table_compat_mode` + - default: `true` + - when `true`, continue writing legacy snapshot tables during migration + - `settings.async_report_generation` + - default: `true` + - when `true`, scheduled jobs defer XLSX generation from the hot path +- Keep existing settings such as: + - `hourly_snapshot_concurrency` + - `monthly_aggregation_granularity` + - retry settings + - cleanup settings +- Scheduled monthly aggregation should ignore hourly granularity unless running a manual or backfill job. + +## Execution Order + +### Phase 1: Hot-Path Runtime Wins +- Add batched hourly writes. +- Decouple report generation from hourly capture. +- Ensure daily scheduled aggregation reads only from `vm_hourly_stats`. +- Ensure monthly scheduled aggregation reads only from `vm_daily_rollup`. +- Keep compatibility tables enabled. +- Define the UI token layer and shared component mapping before page-level redesign work begins. + +### Phase 2: Canonical Dataflow +- Refactor reconciliation so canonical caches are updated first. +- Reduce or eliminate prior-snapshot table mutations during capture. +- Make scheduled aggregation paths canonical-only. +- Keep fallback and repair code for legacy unions/scans. +- Implement the shared page shell, navigation, button, card, table, and form refinements across the existing Templ views. + +### Phase 3: Postgres-Ready Scale-Up +- Validate index coverage on canonical tables. +- Add PostgreSQL partitioning for `vm_hourly_stats`. +- Benchmark Go and SQL aggregation paths on representative production-scale data. +- Keep Go as default unless SQL demonstrates a clear, repeatable runtime win on canonical Postgres data. +- Treat the benchmark as a comparison against a canonical-table SQL implementation, not the current snapshot-union SQL path. +- If SQL wins, promote SQL behind a controlled rollout flag first, then make it default. +- Complete page-specific UI refinement for dashboard, snapshots, vCenter totals, and VM trace using the shared tokenized design system. + +### Phase 4: Compatibility Reduction +- Keep legacy table output behind `snapshot_table_compat_mode`. +- Once canonical-path validation is complete, allow disabling legacy hourly table generation in scheduled runs. +- Retain explicit backfill and rebuild commands for compatibility tables and reports. +- Clean up obsolete styling rules and duplicated visual patterns once the new UI system is fully adopted. + +## Test Plan + +### Correctness Tests +- Add golden-result tests comparing old and new daily outputs for the same synthetic hourly dataset. +- Add golden-result tests comparing old and new monthly outputs for the same synthetic daily dataset. +- Include edge cases for: + - partial-day VM presence + - missing creation times + - deletion-time refinement + - pool changes + - CPU and RAM changes across samples + - VMs identified by `VmId`, `VmUuid`, and fallback name matching + +### Integration Tests +- Hourly capture writes `vm_hourly_stats`, lifecycle caches, and vCenter totals correctly. +- Daily aggregation reads canonical hourly data without scanning `inventory_hourly_*`. +- Monthly aggregation reads canonical daily rollup without scanning hourly history in the normal path. +- `vcenter_aggregate_totals` remains correct for hourly, daily, and monthly views. +- Trace and totals endpoints keep returning equivalent results before and after migration. +- UI page rendering remains valid for dashboard, snapshot pages, vCenter totals, and VM trace after shared component changes. + +### Compatibility Tests +- When `snapshot_table_compat_mode=true`, compatibility snapshot tables still exist and are populated. +- Reports still generate correctly from migrated data. +- Backfill and repair flows can rebuild compatibility outputs from canonical sources. +- UI remains functional when auth is disabled and when auth is enabled with protected API usage documented in-page. + +### Performance Tests +- Measure per-vCenter capture duration. +- Measure hourly write throughput. +- Measure daily aggregation runtime. +- Measure monthly aggregation runtime. +- Measure report generation runtime when decoupled from scheduled jobs. +- Capture baseline metrics before refactor and compare after each phase. +- Measure basic UI payload impact after the refresh so stylesheet and JS growth stay controlled. + +### UI Validation +- Verify token usage in shared CSS so colors, radii, and shadows are not hard-coded inconsistently across pages. +- Verify responsive behavior for dashboard, snapshot tables, vCenter totals, and VM trace at mobile and desktop widths. +- Verify focus states, contrast, and keyboard access for links, buttons, inputs, and table navigation surfaces. +- Verify that the auth guidance on the dashboard still matches actual route protection and Bearer-token behavior. + +## Acceptance Criteria +- Scheduled hourly capture runtime is materially reduced without changing user-visible outputs. +- Scheduled daily aggregation no longer depends on `inventory_hourly_*` scans. +- Scheduled monthly aggregation no longer depends on hourly-history scans. +- Canonical caches become the source of truth for normal scheduled processing. +- Legacy compatibility behavior remains available during migration. +- Existing endpoints, reports, auth behavior, and operational commands continue to work. +- The UI reflects the design direction in `design.md` through tokenized colors, typography, spacing, radius, and shadow usage. +- The dashboard, snapshot pages, vCenter totals view, and VM trace view share a coherent visual system and clearer information hierarchy. +- The refreshed UI remains responsive, accessible, and compatible with the current Templ-based rendering model. + +## Assumptions +- Target direction is Postgres-ready and runtime-first. +- Existing endpoints, report filenames, and user-visible semantics must remain stable. +- SQLite remains supported for development, tests, and smaller installs. +- PostgreSQL is the intended scale-up target for larger environments. +- Compatibility snapshot tables should remain enabled by default until canonical-path validation is complete.