updated UI
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-04-17 15:21:27 +10:00
parent 7848557002
commit 98e92a8264
14 changed files with 1566 additions and 855 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
package core package core
templ Footer() { templ Footer() {
<footer class="fixed p-1 bottom-0 bg-gray-100 w-full border-t"> <footer class="web2-footer" role="contentinfo">
<div class="rounded-lg p-4 text-xs italic text-gray-700 text-center"> <div class="web2-footer-inner">
&copy; Nathan Coad (nathan.coad@dell.com) &copy; Nathan Coad (nathan.coad@dell.com)
</div> </div>
</footer> </footer>
+2 -2
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977 // templ: version: v0.3.1001
package core package core
//lint:file-ignore SA4006 This context is only used if a nested component is present. //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 templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<footer class=\"fixed p-1 bottom-0 bg-gray-100 w-full border-t\"><div class=\"rounded-lg p-4 text-xs italic text-gray-700 text-center\">&copy; Nathan Coad (nathan.coad@dell.com)</div></footer>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<footer class=\"web2-footer\" role=\"contentinfo\"><div class=\"web2-footer-inner\">&copy; Nathan Coad (nathan.coad@dell.com)</div></footer>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+3 -1
View File
@@ -6,7 +6,9 @@ templ Header() {
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="vCTP API endpoint"/> <meta name="description" content="vCTP dashboard and API endpoint"/>
<meta name="color-scheme" content="light"/>
<meta name="theme-color" content="#1b61c9"/>
<title>vCTP API</title> <title>vCTP API</title>
<link rel="icon" href="/favicon.ico"/> <link rel="icon" href="/favicon.ico"/>
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/>
+4 -4
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977 // templ: version: v0.3.1001
package core package core
//lint:file-ignore SA4006 This context is only used if a nested component is present. //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 templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"vCTP API endpoint\"><title>vCTP API</title><link rel=\"icon\" href=\"/favicon.ico\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.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=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"vCTP dashboard and API endpoint\"><meta name=\"color-scheme\" content=\"light\"><meta name=\"theme-color\" content=\"#1b61c9\"><title>vCTP API</title><link rel=\"icon\" href=\"/favicon.ico\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.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=\"")
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("/assets/js/web3-charts.js?v=" + version.Value) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("/assets/js/web3-charts.js?v=" + version.Value)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/header.templ`, Line: 15, Col: 62} return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/header.templ`, Line: 17, Col: 62}
} }
_, 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 {
@@ -51,7 +51,7 @@ func Header() templ.Component {
var templ_7745c5c3_Var3 templ.SafeURL var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs("/assets/css/output@" + version.Value + ".css") templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs("/assets/css/output@" + version.Value + ".css")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/header.templ`, Line: 16, Col: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/header.templ`, Line: 18, Col: 61}
} }
_, 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 {
+53
View File
@@ -0,0 +1,53 @@
package core
type ActionLink struct {
Label string
Href string
Class string
}
type SegmentedLink struct {
Label string
Href string
Class string
}
templ PageHeader(pill string, title string, subtitle string, actions []ActionLink) {
<div class="web2-page-head-row">
<div class="web2-head-copy">
if pill != "" {
<div class="web2-pill">{ pill }</div>
}
<h1 class="web2-page-title">{ title }</h1>
if subtitle != "" {
<p class="web2-page-subtitle">{ subtitle }</p>
}
</div>
if len(actions) > 0 {
<div class="web2-actions">
for _, action := range actions {
<a class={ action.Class } href={ action.Href }>{ action.Label }</a>
}
</div>
}
</div>
}
templ SegmentedActions(actions []SegmentedLink) {
if len(actions) > 0 {
<div class="web3-button-group">
for _, action := range actions {
<a class={ action.Class } href={ action.Href }>{ action.Label }</a>
}
</div>
}
}
templ SectionHead(title string, badge string) {
<div class="web2-section-head">
<h2>{ title }</h2>
if badge != "" {
<span class="web2-badge">{ badge }</span>
}
</div>
}
+29 -33
View File
@@ -1,8 +1,6 @@
package views package views
import ( import "vctp/components/core"
"vctp/components/core"
)
type BuildInfo struct { type BuildInfo struct {
BuildTime string BuildTime string
@@ -15,59 +13,57 @@ templ Index(info BuildInfo) {
<html lang="en"> <html lang="en">
@core.Header() @core.Header()
<body class="flex flex-col min-h-screen web2-bg"> <body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell space-y-8"> <main class="flex-grow web2-shell web2-card-grid">
<section class="web2-header"> <section class="web2-header web2-page-head">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div class="web2-page-head-row">
<div> <div class="web2-head-copy">
<div class="web2-pill">vCTP Console</div> <div class="web2-pill">vCTP Console</div>
<h1 class="mt-3 text-4xl font-bold">Chargeback Intelligence Dashboard</h1> <h1 class="web2-page-title">Chargeback Intelligence Dashboard</h1>
<p class="mt-2 text-sm text-slate-600">Point in time snapshots of consumption with LDAP/JWT protected API access.</p> <p class="web2-page-subtitle">Point-in-time snapshots of vSphere consumption with LDAP and JWT-protected API access.</p>
</div> </div>
<div class="web2-button-group"> <div class="web2-actions">
<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="/vm/trace">VM Trace</a> <a class="web2-button" href="/vm/trace">VM Trace</a>
<a class="web2-button" href="/vcenters">vCenters</a> <a class="web2-button secondary" href="/vcenters">vCenters</a>
<a class="web2-button" href="/swagger/">Swagger UI</a> <a class="web2-button secondary" href="/swagger/">Swagger UI</a>
</div> </div>
</div> </div>
<div class="web2-note">
When authentication is enabled, obtain a token from <code class="web2-code">POST /api/auth/login</code> and send it as <code class="web2-code">Authorization: Bearer &lt;token&gt;</code>. Role policy: <code class="web2-code">viewer</code> covers read/report APIs, <code class="web2-code">admin</code> covers mutating/admin APIs (and includes viewer). UI pages and <code class="web2-code">/metrics</code> remain public.
</div>
</section> </section>
<section class="web2-kpi-grid">
<section class="grid gap-6 md:grid-cols-3">
<div class="web2-card"> <div class="web2-card">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Build Time</p> <p class="web2-kpi-label">Build Time</p>
<p class="mt-3 text-xl font-semibold">{info.BuildTime}</p> <p class="web2-kpi-value">{ info.BuildTime }</p>
</div> </div>
<div class="web2-card"> <div class="web2-card">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">SHA1 Version</p> <p class="web2-kpi-label">SHA1 Version</p>
<p class="mt-3 text-xl font-semibold">{info.SHA1Ver}</p> <p class="web2-kpi-value">{ info.SHA1Ver }</p>
</div> </div>
<div class="web2-card"> <div class="web2-card">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Go Runtime</p> <p class="web2-kpi-label">Go Runtime</p>
<p class="mt-3 text-xl font-semibold">{info.GoVersion}</p> <p class="web2-kpi-value">{ info.GoVersion }</p>
</div> </div>
</section> </section>
<section class="grid gap-6 lg:grid-cols-3"> <section class="grid gap-6 lg:grid-cols-3">
<div class="web2-card"> <div class="web2-card">
<h2 class="text-lg font-semibold mb-2">Overview</h2> <h2 class="mb-2">Overview</h2>
<p class="mt-2 text-sm text-slate-600"> <p class="web2-page-subtitle">
vCTP is a vSphere Chargeback Tracking Platform. vCTP is a vSphere Chargeback Tracking Platform.
</p> </p>
<p class="mt-2 text-sm text-slate-600"> <p class="web2-page-subtitle">
Use fast vCenter totals views (Daily Aggregated and Hourly Detail 45d) and VM Trace views (Hourly Detail and Daily Aggregated) to move between long-range trends and granular timelines. Use fast vCenter totals views (Daily Aggregated and Hourly Detail 45d) and VM Trace views (Hourly Detail and Daily Aggregated) to move between long-range trends and granular timelines.
</p> </p>
<p class="mt-2 text-sm text-slate-600"> <p class="web2-page-subtitle">
When authentication is enabled, obtain a token from <code class="web2-code">POST /api/auth/login</code> and send it as <code class="web2-code">Authorization: Bearer &lt;token&gt;</code>. Use <code class="web2-code">/api/auth/me</code> to inspect active claims and roles during integration and diagnostics.
</p>
<p class="mt-2 text-sm text-slate-600">
Role policy: <code class="web2-code">viewer</code> role covers read/report APIs, and <code class="web2-code">admin</code> role covers mutating/admin APIs (<code class="web2-code">admin</code> also grants viewer access). UI pages and <code class="web2-code">/metrics</code> remain public.
</p> </p>
</div> </div>
<div class="web2-card"> <div class="web2-card">
<h2 class="text-lg font-semibold mb-2">Snapshots and Reports</h2> <h2 class="mb-2">Snapshots and Reports</h2>
<div class="mt-3 text-sm text-slate-600 web2-paragraphs"> <div class="web2-paragraphs web2-page-subtitle">
<p>Hourly snapshots capture inventory per vCenter (concurrency via <code class="web2-code">hourly_snapshot_concurrency</code>), then daily and monthly summaries are derived from those snapshots.</p> <p>Hourly snapshots capture inventory per vCenter (concurrency via <code class="web2-code">hourly_snapshot_concurrency</code>), then daily and monthly summaries are derived from those snapshots.</p>
<p><strong>Hourly tracks:</strong> VM identity (<code class="web2-code">InventoryId</code>, <code class="web2-code">Name</code>, <code class="web2-code">VmId</code>, <code class="web2-code">VmUuid</code>, <code class="web2-code">Vcenter</code>, <code class="web2-code">EventKey</code>, <code class="web2-code">CloudId</code>), lifecycle (<code class="web2-code">CreationTime</code>, <code class="web2-code">DeletionTime</code>, <code class="web2-code">SnapshotTime</code>), placement (<code class="web2-code">Datacenter</code>, <code class="web2-code">Cluster</code>, <code class="web2-code">Folder</code>, <code class="web2-code">ResourcePool</code>), and sizing/state (<code class="web2-code">VcpuCount</code>, <code class="web2-code">RamGB</code>, <code class="web2-code">ProvisionedDisk</code>, <code class="web2-code">PoweredOn</code>, <code class="web2-code">IsTemplate</code>, <code class="web2-code">SrmPlaceholder</code>).</p> <p><strong>Hourly tracks:</strong> VM identity (<code class="web2-code">InventoryId</code>, <code class="web2-code">Name</code>, <code class="web2-code">VmId</code>, <code class="web2-code">VmUuid</code>, <code class="web2-code">Vcenter</code>, <code class="web2-code">EventKey</code>, <code class="web2-code">CloudId</code>), lifecycle (<code class="web2-code">CreationTime</code>, <code class="web2-code">DeletionTime</code>, <code class="web2-code">SnapshotTime</code>), placement (<code class="web2-code">Datacenter</code>, <code class="web2-code">Cluster</code>, <code class="web2-code">Folder</code>, <code class="web2-code">ResourcePool</code>), and sizing/state (<code class="web2-code">VcpuCount</code>, <code class="web2-code">RamGB</code>, <code class="web2-code">ProvisionedDisk</code>, <code class="web2-code">PoweredOn</code>, <code class="web2-code">IsTemplate</code>, <code class="web2-code">SrmPlaceholder</code>).</p>
<p><strong>Daily tracks:</strong> <code class="web2-code">SamplesPresent</code>, <code class="web2-code">TotalSamples</code>, <code class="web2-code">AvgIsPresent</code>, <code class="web2-code">AvgVcpuCount</code>, <code class="web2-code">AvgRamGB</code>, <code class="web2-code">AvgProvisionedDisk</code>, <code class="web2-code">PoolTinPct</code>, <code class="web2-code">PoolBronzePct</code>, <code class="web2-code">PoolSilverPct</code>, <code class="web2-code">PoolGoldPct</code>, plus chargeback totals columns <code class="web2-code">Tin</code>, <code class="web2-code">Bronze</code>, <code class="web2-code">Silver</code>, <code class="web2-code">Gold</code>.</p> <p><strong>Daily tracks:</strong> <code class="web2-code">SamplesPresent</code>, <code class="web2-code">TotalSamples</code>, <code class="web2-code">AvgIsPresent</code>, <code class="web2-code">AvgVcpuCount</code>, <code class="web2-code">AvgRamGB</code>, <code class="web2-code">AvgProvisionedDisk</code>, <code class="web2-code">PoolTinPct</code>, <code class="web2-code">PoolBronzePct</code>, <code class="web2-code">PoolSilverPct</code>, <code class="web2-code">PoolGoldPct</code>, plus chargeback totals columns <code class="web2-code">Tin</code>, <code class="web2-code">Bronze</code>, <code class="web2-code">Silver</code>, <code class="web2-code">Gold</code>.</p>
@@ -81,8 +77,8 @@ templ Index(info BuildInfo) {
</div> </div>
</div> </div>
<div class="web2-card"> <div class="web2-card">
<h2 class="text-lg font-semibold mb-2">Prorating and Aggregation</h2> <h2 class="mb-2">Prorating and Aggregation</h2>
<div class="mt-3 space-y-2 text-sm text-slate-600 web2-paragraphs"> <div class="web2-paragraphs web2-page-subtitle">
<p><code class="web2-code">SamplesPresent</code> is the count of snapshots in which the VM appears; <code class="web2-code">TotalSamples</code> is the count of unique snapshot times for that vCenter/day.</p> <p><code class="web2-code">SamplesPresent</code> is the count of snapshots in which the VM appears; <code class="web2-code">TotalSamples</code> is the count of unique snapshot times for that vCenter/day.</p>
<p><code class="web2-code">AvgIsPresent = SamplesPresent / TotalSamples</code> (0 when <code class="web2-code">TotalSamples</code> is 0).</p> <p><code class="web2-code">AvgIsPresent = SamplesPresent / TotalSamples</code> (0 when <code class="web2-code">TotalSamples</code> is 0).</p>
<p>Daily <code class="web2-code">AvgVcpuCount</code>, <code class="web2-code">AvgRamGB</code>, and <code class="web2-code">AvgProvisionedDisk</code> are per-sample sums divided by <code class="web2-code">TotalSamples</code> (time-weighted).</p> <p>Daily <code class="web2-code">AvgVcpuCount</code>, <code class="web2-code">AvgRamGB</code>, and <code class="web2-code">AvgProvisionedDisk</code> are per-sample sums divided by <code class="web2-code">TotalSamples</code> (time-weighted).</p>
File diff suppressed because one or more lines are too long
+74 -56
View File
@@ -1,6 +1,9 @@
package views package views
import "vctp/components/core" import (
"fmt"
"vctp/components/core"
)
type SnapshotEntry struct { type SnapshotEntry struct {
Label string Label string
@@ -52,23 +55,24 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
<html lang="en"> <html lang="en">
@core.Header() @core.Header()
<body class="flex flex-col min-h-screen web2-bg"> <body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell space-y-8"> <main class="flex-grow web2-shell web2-card-grid">
<section class="web2-header"> <section class="web2-header web2-page-head">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> @core.PageHeader(
<div> "Snapshot Library",
<div class="web2-pill">Snapshot Library</div> title,
<h1 class="mt-3 text-4xl font-bold">{ title }</h1> subtitle,
<p class="mt-2 text-sm text-slate-600">{ subtitle }</p> []core.ActionLink{
</div> {
<a class="web2-button" href="/">Back to Dashboard</a> Label: "Back to Dashboard",
</div> Href: "/",
Class: "web2-button secondary",
},
},
)
</section> </section>
<section class="web2-card"> <section class="web2-card">
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap"> @core.SectionHead("Available Exports", fmt.Sprintf("%d files", len(entries)))
<h2 class="text-lg font-semibold">Available Exports</h2> <div class="web2-table-shell">
<span class="web2-badge">{ len(entries) } files</span>
</div>
<div class="overflow-hidden border border-slate-200 rounded">
<table class="web2-table"> <table class="web2-table">
<thead> <thead>
<tr> <tr>
@@ -81,13 +85,13 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
for i, entry := range entries { for i, entry := range entries {
if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) { if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) {
<tr class="web2-group-row"> <tr class="web2-group-row">
<td colspan="3" class="font-semibold text-slate-700">{ entry.Group }</td> <td colspan="3" class="font-semibold">{ entry.Group }</td>
</tr> </tr>
} }
<tr> <tr>
<td> <td>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-sm font-semibold text-slate-700">{ entry.Label }</span> <span class="text-sm font-semibold">{ entry.Label }</span>
</div> </div>
</td> </td>
<td> <td>
@@ -113,23 +117,24 @@ templ VcenterList(links []VcenterLink) {
<html lang="en"> <html lang="en">
@core.Header() @core.Header()
<body class="flex flex-col min-h-screen web2-bg"> <body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell space-y-8"> <main class="flex-grow web2-shell web2-card-grid">
<section class="web2-header"> <section class="web2-header web2-page-head">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> @core.PageHeader(
<div> "vCenter Inventory",
<div class="web2-pill">vCenter Inventory</div> "Monitored vCenters",
<h1 class="mt-3 text-4xl font-bold">Monitored vCenters</h1> "Select a vCenter to view snapshot totals over time.",
<p class="mt-2 text-sm text-slate-600">Select a vCenter to view snapshot totals over time.</p> []core.ActionLink{
</div> {
<a class="web2-button" href="/">Back to Dashboard</a> Label: "Back to Dashboard",
</div> Href: "/",
Class: "web2-button secondary",
},
},
)
</section> </section>
<section class="web2-card"> <section class="web2-card">
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap"> @core.SectionHead("vCenters", fmt.Sprintf("%d total", len(links)))
<h2 class="text-lg font-semibold">vCenters</h2> <div class="web2-table-shell">
<span class="web2-badge">{ len(links) } total</span>
</div>
<div class="overflow-hidden border border-slate-200 rounded">
<table class="web2-table"> <table class="web2-table">
<thead> <thead>
<tr> <tr>
@@ -140,7 +145,7 @@ templ VcenterList(links []VcenterLink) {
<tbody> <tbody>
for _, link := range links { for _, link := range links {
<tr> <tr>
<td class="font-semibold text-slate-700">{ link.Name }</td> <td class="font-semibold">{ link.Name }</td>
<td class="text-right"> <td class="text-right">
<a class="web2-link" href={ link.Link }>View Totals</a> <a class="web2-link" href={ link.Link }>View Totals</a>
</td> </td>
@@ -161,29 +166,42 @@ templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcen
<html lang="en"> <html lang="en">
@core.Header() @core.Header()
<body class="flex flex-col min-h-screen web2-bg"> <body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell web2-shell-wide space-y-8 max-w-screen-2xl mx-auto" style="max-width: 1400px; width: 100%;"> <main class="flex-grow web2-shell web2-shell-wide web2-card-grid">
<section class="web2-header"> <section class="web2-header web2-page-head">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> @core.PageHeader(
<div> "vCenter Totals",
<div class="web2-pill">vCenter Totals</div> "Totals for "+vcenter,
<h1 class="mt-3 text-4xl font-bold">Totals for { vcenter }</h1> meta.TypeLabel+" snapshots of VM count, vCPU, and RAM over time.",
<p class="mt-2 text-sm text-slate-600">{ meta.TypeLabel } snapshots of VM count, vCPU, and RAM over time.</p> []core.ActionLink{
</div> {
<div class="flex gap-3"> Label: "All vCenters",
<a class="web2-button secondary" href="/vcenters">All vCenters</a> Href: "/vcenters",
<a class="web2-button" href="/">Dashboard</a> Class: "web2-button secondary",
</div> },
</div> {
<div class="web3-button-group mt-8 mb-3"> Label: "Dashboard",
<a class={ meta.HourlyClass } href={ meta.HourlyLink }>Hourly Detail (45d)</a> Href: "/",
<a class={ meta.DailyClass } href={ meta.DailyLink }>Daily Aggregated</a> Class: "web2-button",
</div> },
},
)
@core.SegmentedActions(
[]core.SegmentedLink{
{
Label: "Hourly Detail (45d)",
Href: meta.HourlyLink,
Class: meta.HourlyClass,
},
{
Label: "Daily Aggregated",
Href: meta.DailyLink,
Class: meta.DailyClass,
},
},
)
</section> </section>
<section class="web2-card"> <section class="web2-card">
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap"> @core.SectionHead(meta.TypeLabel+" Snapshots", fmt.Sprintf("%d records", len(entries)))
<h2 class="text-lg font-semibold">{ meta.TypeLabel } Snapshots</h2>
<span class="web2-badge">{ len(entries) } records</span>
</div>
if chart.ConfigJSON != "" { if chart.ConfigJSON != "" {
<div class="mb-6 overflow-auto"> <div class="mb-6 overflow-auto">
<div class="web3-chart-frame"> <div class="web3-chart-frame">
@@ -198,7 +216,7 @@ templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcen
</script> </script>
</div> </div>
} }
<div class="overflow-hidden border border-slate-200 rounded"> <div class="web2-table-shell">
<table class="web2-table"> <table class="web2-table">
<thead> <thead>
<tr> <tr>
+228 -300
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977 // templ: version: v0.3.1001
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //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 "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime" import templruntime "github.com/a-h/templ/runtime"
import "vctp/components/core" import (
"fmt"
"vctp/components/core"
)
type SnapshotEntry struct { type SnapshotEntry struct {
Label string Label string
@@ -159,114 +162,102 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
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\">Snapshot Library</div><h1 class=\"mt-3 text-4xl font-bold\">") 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 web2-card-grid\"><section class=\"web2-header web2-page-head\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string templ_7745c5c3_Err = core.PageHeader(
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title) "Snapshot Library",
if templ_7745c5c3_Err != nil { title,
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 60, Col: 50} subtitle,
} []core.ActionLink{
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) {
Label: "Back to Dashboard",
Href: "/",
Class: "web2-button secondary",
},
},
).Render(ctx, templ_7745c5c3_Buffer)
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, 3, "</h1><p class=\"mt-2 text-sm text-slate-600\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</section><section class=\"web2-card\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string templ_7745c5c3_Err = core.SectionHead("Available Exports", fmt.Sprintf("%d files", len(entries))).Render(ctx, templ_7745c5c3_Buffer)
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))
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, 4, "</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\">Available Exports</h2><span class=\"web2-badge\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"web2-table-shell\"><table class=\"web2-table\"><thead><tr><th>Snapshot</th><th>Records</th><th class=\"text-right\">Download</th></tr></thead> <tbody>")
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</span></div><div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>Snapshot</th><th>Records</th><th class=\"text-right\">Download</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for i, entry := range entries { for i, entry := range entries {
if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) { if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<tr class=\"web2-group-row\"><td colspan=\"3\" class=\"font-semibold text-slate-700\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<tr class=\"web2-group-row\"><td colspan=\"3\" class=\"font-semibold\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group) templ_7745c5c3_Var5, 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: 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_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " <tr><td><div class=\"flex flex-col\"><span class=\"text-sm font-semibold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: 94, Col: 61}
}
_, 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, 8, "</span></div></td><td><span class=\"web2-badge\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: 98, Col: 49}
}
_, 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, 9, " records</span></td><td class=\"text-right\"><a class=\"web2-link\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 101, Col: 49}
} }
_, 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 {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</td></tr>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">Download XLSX</a></td></tr>")
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, 8, " <tr><td><div class=\"flex flex-col\"><span class=\"text-sm font-semibold text-slate-700\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</tbody></table></div></section></main></body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, 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}
}
_, 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, 9, "</span></div></td><td><span class=\"web2-badge\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, 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}
}
_, 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, 10, " records</span></td><td class=\"text-right\"><a class=\"web2-link\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 templ.SafeURL
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 97, Col: 49}
}
_, 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, 11, "\">Download XLSX</a></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</tbody></table></div></section></main></body>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -274,7 +265,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
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, 13, "</html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -298,12 +289,12 @@ func VcenterList(links []VcenterLink) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var12 := templ.GetChildren(ctx) templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil { if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var12 = templ.NopComponent templ_7745c5c3_Var9 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<!doctype html><html lang=\"en\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -311,34 +302,48 @@ func VcenterList(links []VcenterLink) 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, 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\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell web2-card-grid\"><section class=\"web2-header web2-page-head\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 string templ_7745c5c3_Err = core.PageHeader(
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(len(links)) "vCenter Inventory",
if templ_7745c5c3_Err != nil { "Monitored vCenters",
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 130, Col: 43} "Select a vCenter to view snapshot totals over time.",
} []core.ActionLink{
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) {
Label: "Back to Dashboard",
Href: "/",
Class: "web2-button secondary",
},
},
).Render(ctx, templ_7745c5c3_Buffer)
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, 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>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</section><section class=\"web2-card\">")
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, "<div class=\"web2-table-shell\"><table class=\"web2-table\"><thead><tr><th>vCenter</th><th class=\"text-right\">Totals</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, link := range links { for _, link := range links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<tr><td class=\"font-semibold text-slate-700\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<tr><td class=\"font-semibold\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -346,12 +351,12 @@ func VcenterList(links []VcenterLink) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 templ.SafeURL var templ_7745c5c3_Var11 templ.SafeURL
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -392,9 +397,9 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var16 := templ.GetChildren(ctx) templ_7745c5c3_Var12 := templ.GetChildren(ctx)
if templ_7745c5c3_Var16 == nil { if templ_7745c5c3_Var12 == nil {
templ_7745c5c3_Var16 = templ.NopComponent templ_7745c5c3_Var12 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<!doctype html><html lang=\"en\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<!doctype html><html lang=\"en\">")
@@ -405,214 +410,137 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
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, 23, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell web2-shell-wide space-y-8 max-w-screen-2xl mx-auto\" style=\"max-width: 1400px; width: 100%;\"><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 ") 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 web2-shell-wide web2-card-grid\"><section class=\"web2-header web2-page-head\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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 = 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
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</section><section class=\"web2-card\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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, 25, "<div class=\"mb-6 overflow-auto\"><div class=\"web3-chart-frame\"><canvas id=\"vcenter-totals-chart\" class=\"web3-chart-canvas\" role=\"img\" aria-label=\"Totals over time\" data-chart-config=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 208, Col: 145}
}
_, 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, 26, "\"></canvas><div id=\"vcenter-totals-tooltip\" class=\"web3-chart-tooltip\" aria-hidden=\"true\"></div></div><script>\n\t\t\t\t\t\t\t\twindow.Web3Charts.renderFromDataset({\n\t\t\t\t\t\t\t\t\tcanvasId: \"vcenter-totals-chart\",\n\t\t\t\t\t\t\t\t\ttooltipId: \"vcenter-totals-tooltip\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t</script></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<div class=\"web2-table-shell\"><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, 28, "<tr><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: 232, Col: 30}
}
_, 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, 29, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: 233, Col: 48}
}
_, 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, 30, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: 234, Col: 50}
}
_, 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, 31, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vcenter) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 169, Col: 63} 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_Var17)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
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, 24, "</h1><p class=\"mt-2 text-sm text-slate-600\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td></tr>")
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))
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: 178, Col: 59}
}
_, 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 Detail (45d)</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: 179, Col: 57}
}
_, 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 Aggregated</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_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</h2><span class=\"web2-badge\">")
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</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if chart.ConfigJSON != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"mb-6 overflow-auto\"><div class=\"web3-chart-frame\"><canvas id=\"vcenter-totals-chart\" class=\"web3-chart-canvas\" role=\"img\" aria-label=\"Totals over time\" data-chart-config=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 190, Col: 145}
}
_, 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, 35, "\"></canvas><div id=\"vcenter-totals-tooltip\" class=\"web3-chart-tooltip\" aria-hidden=\"true\"></div></div><script>\n\t\t\t\t\t\t\t\twindow.Web3Charts.renderFromDataset({\n\t\t\t\t\t\t\t\t\tcanvasId: \"vcenter-totals-chart\",\n\t\t\t\t\t\t\t\t\ttooltipId: \"vcenter-totals-tooltip\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t</script></div>")
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, 36, "<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>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</tbody></table></div></section></main></body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, entry := range entries {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<tr><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, 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}
}
_, 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, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, 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}
}
_, 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, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, 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}
}
_, 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, 40, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, 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}
}
_, 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, 41, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</tbody></table></div></section></main></body>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -620,7 +548,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
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, 43, "</html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+53 -45
View File
@@ -48,48 +48,56 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri
<html lang="en"> <html lang="en">
@core.Header() @core.Header()
<body class="flex flex-col min-h-screen web2-bg"> <body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell web2-shell-wide space-y-8 max-w-screen-2xl mx-auto"> <main class="flex-grow web2-shell web2-shell-wide web2-card-grid">
<section class="web2-header"> <section class="web2-header web2-page-head">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> @core.PageHeader(
<div> "VM Trace",
<div class="web2-pill">VM Trace</div> "Snapshot history"+display_query,
<h1 class="mt-3 text-4xl font-bold">Snapshot history{display_query}</h1> "Timeline of vCPU, RAM, and resource pool changes across "+meta.TypeLabel+" snapshots.",
<p class="mt-2 text-sm text-slate-600">Timeline of vCPU, RAM, and resource pool changes across { meta.TypeLabel } snapshots.</p> []core.ActionLink{
</div> {
<div class="flex gap-3 flex-wrap"> Label: "Dashboard",
<a class="web2-button" href="/">Dashboard</a> Href: "/",
</div> Class: "web2-button secondary",
</div> },
<form method="get" action="/vm/trace" class="mt-4 grid gap-3 md:grid-cols-3"> },
)
<form method="get" action="/vm/trace" class="web2-form-grid">
<input type="hidden" name="view" value={ meta.ViewType }/> <input type="hidden" name="view" value={ meta.ViewType }/>
<div class="flex flex-col gap-1"> <div class="web2-field">
<label class="text-sm text-slate-600" for="vm_id">VM ID</label> <label class="web2-label" for="vm_id">VM ID</label>
<input class="web2-card border border-slate-200 px-3 py-2 rounded" type="text" id="vm_id" name="vm_id" value={vm_id} placeholder="vm-12345"/> <input class="web2-input" type="text" id="vm_id" name="vm_id" value={ vm_id } placeholder="vm-12345"/>
</div> </div>
<div class="flex flex-col gap-1"> <div class="web2-field">
<label class="text-sm text-slate-600" for="vm_uuid">VM UUID</label> <label class="web2-label" for="vm_uuid">VM UUID</label>
<input class="web2-card border border-slate-200 px-3 py-2 rounded" type="text" id="vm_uuid" name="vm_uuid" value={vm_uuid} placeholder="uuid..."/> <input class="web2-input" type="text" id="vm_uuid" name="vm_uuid" value={ vm_uuid } placeholder="uuid..."/>
</div> </div>
<div class="flex flex-col gap-1"> <div class="web2-field">
<label class="text-sm text-slate-600" for="name">Name</label> <label class="web2-label" for="name">Name</label>
<input class="web2-card border border-slate-200 px-3 py-2 rounded" type="text" id="name" name="name" value={vm_name} placeholder="VM name"/> <input class="web2-input" type="text" id="name" name="name" value={ vm_name } placeholder="VM name"/>
</div> </div>
<div class="md:col-span-3 flex gap-2"> <div class="web2-form-actions web2-form-actions-full">
<button class="web3-button active" type="submit">Load VM Trace</button> <button class="web3-button active" type="submit">Load VM Trace</button>
<a class="web3-button" href="/vm/trace">Clear</a> <a class="web3-button" href="/vm/trace">Clear</a>
</div> </div>
</form> </form>
<div class="web3-button-group mt-5 mb-1"> @core.SegmentedActions(
<a class={ meta.HourlyClass } href={ meta.HourlyLink }>Hourly Detail</a> []core.SegmentedLink{
<a class={ meta.DailyClass } href={ meta.DailyLink }>Daily Aggregated</a> {
</div> Label: "Hourly Detail",
Href: meta.HourlyLink,
Class: meta.HourlyClass,
},
{
Label: "Daily Aggregated",
Href: meta.DailyLink,
Class: meta.DailyClass,
},
},
)
</section> </section>
<section class="web2-card"> <section class="web2-card">
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap"> @core.SectionHead(meta.TypeLabel+" Timeline", fmt.Sprintf("%d samples", len(entries)))
<h2 class="text-lg font-semibold">{ meta.TypeLabel } Timeline</h2>
<span class="web2-badge">{len(entries)} samples</span>
</div>
if chart.ConfigJSON != "" { if chart.ConfigJSON != "" {
<div class="mb-6 overflow-auto"> <div class="mb-6 overflow-auto">
<div class="web3-chart-frame"> <div class="web3-chart-frame">
@@ -105,28 +113,28 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri
</div> </div>
} }
<div class="grid gap-3 md:grid-cols-2 mb-4"> <div class="grid gap-3 md:grid-cols-2 mb-4">
<div class="web2-card"> <div class="web2-subcard">
<p class="text-xs uppercase tracking-[0.15em] text-slate-500">Creation time</p> <p class="web2-subcard-label">Creation Time</p>
<p class="mt-2 text-base font-semibold text-slate-800">{creationLabel}</p> <p class="web2-subcard-value">{ creationLabel }</p>
if creationApprox { if creationApprox {
<p class="text-xs text-slate-500 mt-1">Approximate (earliest snapshot)</p> <p class="web2-muted web2-caption mt-1">Approximate (earliest snapshot)</p>
} }
</div> </div>
<div class="web2-card"> <div class="web2-subcard">
<p class="text-xs uppercase tracking-[0.15em] text-slate-500">Deletion time</p> <p class="web2-subcard-label">Deletion Time</p>
<p class="mt-2 text-base font-semibold text-slate-800">{deletionLabel}</p> <p class="web2-subcard-value">{ deletionLabel }</p>
</div> </div>
</div> </div>
if diagnostics.Visible && len(diagnostics.Lines) > 0 { if diagnostics.Visible && len(diagnostics.Lines) > 0 {
<details class="web2-card mb-4"> <details class="web2-subcard mb-4">
<summary class="cursor-pointer text-sm font-semibold text-slate-700">Lifecycle diagnostics</summary> <summary class="web2-details-summary">Lifecycle diagnostics</summary>
<div class="mt-3 overflow-hidden border border-slate-200 rounded"> <div class="mt-3 web2-table-shell">
<table class="web2-table"> <table class="web2-table">
<tbody> <tbody>
for _, line := range diagnostics.Lines { for _, line := range diagnostics.Lines {
<tr> <tr>
<td class="font-semibold text-slate-700 w-72">{ line.Label }</td> <td class="font-semibold w-72">{ line.Label }</td>
<td class="text-slate-600">{ line.Value }</td> <td class="web2-muted">{ line.Value }</td>
</tr> </tr>
} }
</tbody> </tbody>
@@ -134,7 +142,7 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri
</div> </div>
</details> </details>
} }
<div class="overflow-hidden border border-slate-200 rounded"> <div class="web2-table-shell">
<table class="web2-table"> <table class="web2-table">
<thead> <thead>
<tr> <tr>
+157 -235
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977 // templ: version: v0.3.1001
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -80,413 +80,335 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin
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 web2-shell-wide space-y-8 max-w-screen-2xl mx-auto\"><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\">VM Trace</div><h1 class=\"mt-3 text-4xl font-bold\">Snapshot history") 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 web2-shell-wide web2-card-grid\"><section class=\"web2-header web2-page-head\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = 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",
},
},
).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<form method=\"get\" action=\"/vm/trace\" class=\"web2-form-grid\"><input type=\"hidden\" name=\"view\" value=\"")
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(display_query) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(meta.ViewType)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 56, Col: 74} return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 66, Col: 60}
} }
_, 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 {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1><p class=\"mt-2 text-sm text-slate-600\">Timeline of vCPU, RAM, and resource pool changes across ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><div class=\"web2-field\"><label class=\"web2-label\" for=\"vm_id\">VM ID</label> <input class=\"web2-input\" type=\"text\" id=\"vm_id\" name=\"vm_id\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(vm_id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 57, Col: 119} return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 69, Col: 82}
} }
_, 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 {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " snapshots.</p></div><div class=\"flex gap-3 flex-wrap\"><a class=\"web2-button\" href=\"/\">Dashboard</a></div></div><form method=\"get\" action=\"/vm/trace\" class=\"mt-4 grid gap-3 md:grid-cols-3\"><input type=\"hidden\" name=\"view\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" placeholder=\"vm-12345\"></div><div class=\"web2-field\"><label class=\"web2-label\" for=\"vm_uuid\">VM UUID</label> <input class=\"web2-input\" type=\"text\" id=\"vm_uuid\" name=\"vm_uuid\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(meta.ViewType) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(vm_uuid)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 64, Col: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 73, Col: 88}
} }
_, 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 {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><div class=\"flex flex-col gap-1\"><label class=\"text-sm text-slate-600\" for=\"vm_id\">VM ID</label> <input class=\"web2-card border border-slate-200 px-3 py-2 rounded\" type=\"text\" id=\"vm_id\" name=\"vm_id\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" placeholder=\"uuid...\"></div><div class=\"web2-field\"><label class=\"web2-label\" for=\"name\">Name</label> <input class=\"web2-input\" type=\"text\" id=\"name\" name=\"name\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(vm_id) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(vm_name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 67, Col: 123} return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 77, Col: 82}
} }
_, 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 {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" placeholder=\"vm-12345\"></div><div class=\"flex flex-col gap-1\"><label class=\"text-sm text-slate-600\" for=\"vm_uuid\">VM UUID</label> <input class=\"web2-card border border-slate-200 px-3 py-2 rounded\" type=\"text\" id=\"vm_uuid\" name=\"vm_uuid\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" placeholder=\"VM name\"></div><div class=\"web2-form-actions web2-form-actions-full\"><button class=\"web3-button active\" type=\"submit\">Load VM Trace</button> <a class=\"web3-button\" href=\"/vm/trace\">Clear</a></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = core.SegmentedActions(
[]core.SegmentedLink{
{
Label: "Hourly Detail",
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
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</section><section class=\"web2-card\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = core.SectionHead(meta.TypeLabel+" Timeline", fmt.Sprintf("%d samples", 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, 9, "<div class=\"mb-6 overflow-auto\"><div class=\"web3-chart-frame\"><canvas id=\"vm-trace-chart\" class=\"web3-chart-canvas\" role=\"img\" aria-label=\"VM timeline\" data-chart-config=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(vm_uuid) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 71, Col: 129} return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 104, Col: 134}
} }
_, 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 {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" placeholder=\"uuid...\"></div><div class=\"flex flex-col gap-1\"><label class=\"text-sm text-slate-600\" for=\"name\">Name</label> <input class=\"web2-card border border-slate-200 px-3 py-2 rounded\" type=\"text\" id=\"name\" name=\"name\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></canvas><div id=\"vm-trace-tooltip\" class=\"web3-chart-tooltip\" aria-hidden=\"true\"></div></div><script>\n\t\t\t\t\t\t\t\twindow.Web3Charts.renderFromDataset({\n\t\t\t\t\t\t\t\t\tcanvasId: \"vm-trace-chart\",\n\t\t\t\t\t\t\t\t\ttooltipId: \"vm-trace-tooltip\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t</script></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"grid gap-3 md:grid-cols-2 mb-4\"><div class=\"web2-subcard\"><p class=\"web2-subcard-label\">Creation Time</p><p class=\"web2-subcard-value\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string 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 { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
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, 8, "\" placeholder=\"VM name\"></div><div class=\"md:col-span-3 flex gap-2\"><button class=\"web3-button active\" type=\"submit\">Load VM Trace</button> <a class=\"web3-button\" href=\"/vm/trace\">Clear</a></div></form><div class=\"web3-button-group mt-5 mb-1\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 = []any{meta.HourlyClass} if creationApprox {
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"web2-muted web2-caption mt-1\">Approximate (earliest snapshot)</p>")
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, 9, "<a class=\"") }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div><div class=\"web2-subcard\"><p class=\"web2-subcard-label\">Deletion Time</p><p class=\"web2-subcard-value\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(deletionLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 125, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if diagnostics.Visible && len(diagnostics.Lines) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<details class=\"web2-subcard mb-4\"><summary class=\"web2-details-summary\">Lifecycle diagnostics</summary><div class=\"mt-3 web2-table-shell\"><table class=\"web2-table\"><tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, line := range diagnostics.Lines {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<tr><td class=\"font-semibold w-72\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String()) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(line.Label)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 1, Col: 0} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
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, 10, "\" href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td class=\"web2-muted\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 templ.SafeURL var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(meta.HourlyLink) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(line.Value)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 83, Col: 59} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
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, 11, "\">Hourly Detail</a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td></tr>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 = []any{meta.DailyClass} }
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</tbody></table></div></details>")
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, 12, "<a class=\"") }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"web2-table-shell\"><table class=\"web2-table\"><thead><tr><th>Snapshot</th><th>VM Name</th><th>VmId</th><th>VmUuid</th><th>Vcenter</th><th>Resource Pool</th><th class=\"text-right\">vCPUs</th><th class=\"text-right\">RAM (GB)</th><th class=\"text-right\">Disk</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, e := range entries {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<tr><td>")
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, "</td><td>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String()) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 1, Col: 0} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
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, 13, "\" href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td><td>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 templ.SafeURL var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(meta.DailyLink) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmId)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 84, Col: 57} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
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, 14, "\">Daily Aggregated</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\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td><td>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmUuid)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 90, Col: 56} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
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, 15, " Timeline</h2><span class=\"web2-badge\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td><td>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vcenter)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 91, Col: 44} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
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, 16, " samples</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if chart.ConfigJSON != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"mb-6 overflow-auto\"><div class=\"web3-chart-frame\"><canvas id=\"vm-trace-chart\" class=\"web3-chart-canvas\" role=\"img\" aria-label=\"VM timeline\" data-chart-config=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string 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 { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
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, 18, "\"></canvas><div id=\"vm-trace-tooltip\" class=\"web3-chart-tooltip\" aria-hidden=\"true\"></div></div><script>\n\t\t\t\t\t\t\t\twindow.Web3Charts.renderFromDataset({\n\t\t\t\t\t\t\t\t\tcanvasId: \"vm-trace-chart\",\n\t\t\t\t\t\t\t\t\ttooltipId: \"vm-trace-tooltip\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t</script></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"grid gap-3 md:grid-cols-2 mb-4\"><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.15em] text-slate-500\">Creation time</p><p class=\"mt-2 text-base font-semibold text-slate-800\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(creationLabel) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(e.VcpuCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 110, Col: 76} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
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, 20, "</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if creationApprox {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p class=\"text-xs text-slate-500 mt-1\">Approximate (earliest snapshot)</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.15em] text-slate-500\">Deletion time</p><p class=\"mt-2 text-base font-semibold text-slate-800\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var18 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(deletionLabel) templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(e.RamGB)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 117, Col: 76} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
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, 23, "</p></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"text-right\">")
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, "<details class=\"web2-card mb-4\"><summary class=\"cursor-pointer text-sm font-semibold text-slate-700\">Lifecycle diagnostics</summary><div class=\"mt-3 overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, line := range diagnostics.Lines {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tr><td class=\"font-semibold text-slate-700 w-72\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var19 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(line.Label) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", e.ProvisionedDisk))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 128, Col: 70} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
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, 26, "</td><td class=\"text-slate-600\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td></tr>")
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, "</td></tr>")
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, 28, "</tbody></table></div></details>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</tbody></table></div></section></main></body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>Snapshot</th><th>VM Name</th><th>VmId</th><th>VmUuid</th><th>Vcenter</th><th>Resource Pool</th><th class=\"text-right\">vCPUs</th><th class=\"text-right\">RAM (GB)</th><th class=\"text-right\">Disk</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, e := range entries {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<tr><td>")
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, "</td><td>")
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, "</td><td>")
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, "</td><td>")
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, "</td><td>")
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, "</td><td>")
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, "</td><td class=\"text-right\">")
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, "</td><td class=\"text-right\">")
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, "</td><td class=\"text-right\">")
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, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</tbody></table></div></section></main></body>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+89
View File
@@ -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.08px0.28px)
- 12px radius buttons, 16px32px 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 | 400500 | 1.151.25 | normal |
| Card Title | Haas | 24px | 400 | 1.201.30 | 0.12px |
| Feature | Haas | 20px | 400 | 1.251.50 | 0.1px |
| Body | Haas | 18px | 400 | 1.35 | 0.18px |
| Body Medium | Haas | 16px | 500 | 1.30 | 0.080.16px |
| Button | Haas | 16px | 500 | 1.251.30 | 0.08px |
| Caption | Haas | 14px | 400500 | 1.251.35 | 0.070.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`, 16px24px radius
### Inputs: Standard Haas styling
## 5. Layout
- Spacing: 148px (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: 4251664px (23 breakpoints)
## 9. Agent Prompt Guide
- Text: Deep Navy (`#181d26`)
- CTA: Airtable Blue (`#1b61c9`)
- Background: White (`#ffffff`)
- Border: `#e0e2e6`
+458 -91
View File
@@ -1,45 +1,182 @@
:root { :root {
--web2-blue: #1d9bf0; --theme_text_primary: #181d26;
--web2-slate: #0f172a; --theme_text_weak: rgba(4, 14, 32, 0.69);
--web2-muted: #64748b; --theme_text_inverse: rgba(249, 252, 255, 0.97);
--web2-card: #ffffff; --theme_text_secondary_active: rgba(7, 12, 20, 0.82);
--web2-border: #e5e7eb; --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 { body {
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; margin: 0;
color: var(--web2-slate); 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 { .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 { .web2-shell {
max-width: 1100px; max-width: 1140px;
margin: 0 auto; margin: 0 auto;
padding: 2rem 1.5rem 4rem; padding: 2.5rem 1.5rem 2.25rem;
} }
.web2-shell-wide { .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 { .web2-header {
background: var(--web2-card); background: var(--theme_surface_section);
border: 1px solid var(--web2-border); border: 1px solid var(--theme_border);
border-radius: 4px; border-radius: var(--theme_radius_section);
padding: 1.5rem 2rem; padding: 1.65rem 2rem;
box-shadow: var(--theme_shadow_card), var(--theme_shadow_soft);
} }
.web2-card { .web2-card {
background: var(--web2-card); background: var(--theme_surface_primary);
border: 1px solid var(--web2-border); border: 1px solid var(--theme_border);
border-radius: 4px; border-radius: var(--theme_radius_card);
padding: 1.5rem 1.75rem; padding: 1.4rem 1.6rem;
box-shadow: var(--theme_shadow_card);
} }
.web2-card h2 { .web2-card h2 {
position: relative; position: relative;
padding-left: 0.75rem; padding-left: 0.9rem;
font-size: 1.05rem; font-size: 1.1rem;
font-weight: 700; font-family: var(--theme_font_display);
letter-spacing: 0.02em; font-weight: 600;
color: #0b1220; letter-spacing: var(--theme_letter_button);
color: var(--theme_text_primary);
} }
.web2-card h2::before { .web2-card h2::before {
content: ""; content: "";
position: absolute; position: absolute;
@@ -47,191 +184,355 @@ body {
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
width: 4px; width: 4px;
height: 70%; height: 72%;
background: var(--web2-blue); background: var(--theme_accent_blue);
border-radius: 2px; border-radius: 999px;
box-shadow: 0 0 0 1px rgba(29, 155, 240, 0.18); box-shadow: 0 0 0 1px rgba(27, 97, 201, 0.22);
} }
.web2-pill { .web2-pill {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
background: #f8fafc; background: var(--theme_surface_subtle);
border: 1px solid var(--web2-border); border: 1px solid var(--theme_border);
color: var(--web2-muted); color: var(--theme_text_weak);
padding: 0.2rem 0.6rem; padding: 0.28rem 0.72rem;
border-radius: 3px; border-radius: var(--theme_radius_button);
font-size: 0.85rem; font-size: 0.82rem;
letter-spacing: 0.02em; font-weight: 500;
letter-spacing: 0.24px;
} }
.web2-code { .web2-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: var(--theme_font_code);
background: #f1f5f9; background: rgba(27, 97, 201, 0.07);
border: 1px solid var(--web2-border); border: 1px solid var(--theme_border);
border-radius: 3px; border-radius: 8px;
padding: 0.1rem 0.35rem; padding: 0.12rem 0.42rem;
font-size: 0.85em; font-size: 0.84em;
color: #0f172a; color: var(--theme_text_primary);
} }
.web2-paragraphs p + p { .web2-paragraphs p + p {
margin-top: 0.85rem; margin-top: 0.85rem;
} }
.web2-link { .web2-link {
color: var(--web2-blue); color: var(--theme_accent_blue);
text-decoration: none; text-decoration: none;
font-weight: 600; font-weight: 600;
transition: color var(--theme_transition_fast), text-decoration-color var(--theme_transition_fast);
} }
.web2-link:hover { .web2-link:hover {
color: var(--theme_accent_blue_hover);
text-decoration: underline; text-decoration: underline;
} }
.web2-button { .web2-button {
background: var(--web2-blue); background: var(--theme_accent_blue);
color: #fff; color: var(--theme_text_inverse);
padding: 0.45rem 0.9rem; padding: 0.56rem 1rem;
border-radius: 3px; border-radius: var(--theme_radius_button);
border: 1px solid #1482d0; border: 1px solid rgba(22, 79, 166, 0.95);
box-shadow: none; box-shadow: var(--theme_shadow_button);
font-weight: 600; font-weight: 600;
letter-spacing: var(--theme_letter_button);
text-decoration: none; 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 { .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 { .web2-button-group {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.web2-button-group .web2-button { .web2-button-group .web2-button {
margin: 0 0.5rem 0.5rem 0; margin: 0 0.5rem 0.5rem 0;
} }
.web3-button { .web3-button {
background: #f3f4f6; background: var(--theme_surface_subtle);
color: #0f172a; color: var(--theme_text_primary);
padding: 0.5rem 1rem; padding: 0.52rem 1.02rem;
border-radius: 6px; border-radius: var(--theme_radius_button);
border: 1px solid #e5e7eb; border: 1px solid var(--theme_border);
text-decoration: none; text-decoration: none;
font-weight: 600; 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; display: inline-flex;
align-items: center; align-items: center;
gap: 0.35rem; gap: 0.35rem;
} }
.web3-button:hover { .web3-button:hover {
background: #e2e8f0; background: #edf2fa;
border-color: #cbd5e1; border-color: #c8d2e1;
} }
.web3-button.active { .web3-button.active {
background: #dbeafe; background: var(--theme_accent_blue_soft);
border-color: #93c5fd; border-color: rgba(27, 97, 201, 0.45);
color: #1d4ed8; color: var(--theme_accent_blue);
box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.35); box-shadow: rgba(45, 127, 249, 0.28) 0 0 0 2px;
} }
.web3-button-group { .web3-button-group {
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 4px; margin-top: 4px;
} }
.web2-list li {
background: #ffffff; .web2-table-shell {
border: 1px solid var(--web2-border); overflow-x: auto;
border-radius: 3px; overflow-y: hidden;
padding: 0.75rem 1rem; border: 1px solid var(--theme_border);
box-shadow: none; 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 { .web2-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 0.95rem; font-size: 0.95rem;
letter-spacing: 0.08px;
min-width: 560px;
} }
.web2-table thead th { .web2-table thead th {
text-align: left; text-align: left;
padding: 0.75rem 0.5rem; padding: 0.8rem 0.55rem;
font-weight: 700; font-weight: 700;
color: var(--web2-muted); color: var(--theme_text_weak);
border-bottom: 1px solid var(--web2-border); letter-spacing: var(--theme_letter_caption);
border-bottom: 1px solid var(--theme_border);
background: rgba(248, 250, 252, 0.95);
} }
.web2-table tbody td { .web2-table tbody td {
padding: 0.9rem 0.5rem; padding: 0.92rem 0.55rem;
border-bottom: 1px solid var(--web2-border); border-bottom: 1px solid var(--theme_border);
} }
.web2-table tbody tr:nth-child(odd) { .web2-table tbody tr:nth-child(odd) {
background: #f8fafc; background: var(--theme_surface_subtle);
} }
.web2-table tbody tr:nth-child(even) { .web2-table tbody tr:nth-child(even) {
background: #ffffff; background: var(--theme_surface_primary);
} }
.web2-group-row td { .web2-group-row td {
background: #e8eef5; background: #eaf1fb;
color: #0f172a; color: var(--theme_text_primary);
border-bottom: 1px solid var(--web2-border); border-bottom: 1px solid var(--theme_border);
padding: 0.65rem 0.5rem; padding: 0.7rem 0.55rem;
} }
.web2-badge { .web2-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.25rem;
border: 1px solid var(--web2-border); border: 1px solid var(--theme_border);
padding: 0.15rem 0.45rem; padding: 0.16rem 0.5rem;
border-radius: 3px; border-radius: var(--theme_radius_button);
font-size: 0.8rem; font-size: 0.8rem;
color: var(--web2-muted); letter-spacing: var(--theme_letter_caption);
background: #f8fafc; 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 { .web3-chart-frame {
position: relative; position: relative;
min-width: 760px; min-width: 760px;
width: 100%; width: 100%;
} }
.web3-chart-canvas { .web3-chart-canvas {
display: block; display: block;
width: 100%; width: 100%;
height: 360px; height: 360px;
background: #ffffff; background: var(--theme_surface_primary);
border: 1px solid #e2e8f0; border: 1px solid var(--theme_border);
border-radius: 6px; border-radius: var(--theme_radius_card);
} }
.web3-chart-tooltip { .web3-chart-tooltip {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
background: rgba(15, 23, 42, 0.95); background: rgba(24, 29, 38, 0.97);
color: #f8fafc; color: var(--theme_text_inverse);
padding: 0.55rem 0.65rem; padding: 0.55rem 0.65rem;
border-radius: 6px; border-radius: var(--theme_radius_button);
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1.35; line-height: 1.35;
min-width: 170px; min-width: 170px;
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.25); box-shadow: 0 10px 30px rgba(2, 6, 23, 0.25);
z-index: 20; z-index: 20;
transition: opacity 0.08s linear; transition: opacity 80ms linear;
} }
.web3-chart-tooltip.visible { .web3-chart-tooltip.visible {
opacity: 1; opacity: 1;
} }
.web3-chart-tooltip-title { .web3-chart-tooltip-title {
font-weight: 700; font-weight: 700;
color: #e2e8f0; color: #e2e8f0;
margin-bottom: 0.35rem; margin-bottom: 0.35rem;
} }
.web3-chart-tooltip-row { .web3-chart-tooltip-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.65rem; gap: 0.65rem;
} }
.web3-chart-tooltip-label { .web3-chart-tooltip-label {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
color: #cbd5e1; color: #cbd5e1;
} }
.web3-chart-tooltip-value { .web3-chart-tooltip-value {
font-weight: 700; font-weight: 700;
color: #f8fafc; color: #f8fafc;
} }
.web3-chart-tooltip-swatch { .web3-chart-tooltip-swatch {
display: inline-block; display: inline-block;
width: 8px; width: 8px;
@@ -239,8 +540,74 @@ body {
border-radius: 999px; border-radius: 999px;
margin-right: 0.35rem; 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) { @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 { .web3-chart-frame {
min-width: 640px; 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));
}
} }
+330
View File
@@ -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_<epoch>` 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 users 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.