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
+5 -5
View File
@@ -1,9 +1,9 @@
package core
templ Footer() {
<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>
<footer class="web2-footer" role="contentinfo">
<div class="web2-footer-inner">
&copy; Nathan Coad (nathan.coad@dell.com)
</div>
</footer>
}
+2 -2
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
// templ: version: v0.3.1001
package core
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -29,7 +29,7 @@ func Footer() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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 {
return templ_7745c5c3_Err
}
+3 -1
View File
@@ -6,7 +6,9 @@ templ Header() {
<head>
<meta charset="UTF-8"/>
<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>
<link rel="icon" href="/favicon.ico"/>
<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.
// templ: version: v0.3.977
// templ: version: v0.3.1001
package core
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -31,14 +31,14 @@ func Header() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("/assets/js/web3-charts.js?v=" + version.Value)
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))
if templ_7745c5c3_Err != nil {
@@ -51,7 +51,7 @@ func Header() templ.Component {
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs("/assets/css/output@" + version.Value + ".css")
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))
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>
}
+56 -60
View File
@@ -1,8 +1,6 @@
package views
import (
"vctp/components/core"
)
import "vctp/components/core"
type BuildInfo struct {
BuildTime string
@@ -15,83 +13,81 @@ templ Index(info BuildInfo) {
<html lang="en">
@core.Header()
<body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell space-y-8">
<section class="web2-header">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<main class="flex-grow web2-shell web2-card-grid">
<section class="web2-header web2-page-head">
<div class="web2-page-head-row">
<div class="web2-head-copy">
<div class="web2-pill">vCTP Console</div>
<h1 class="mt-3 text-4xl font-bold">Chargeback Intelligence Dashboard</h1>
<p class="mt-2 text-sm text-slate-600">Point in time snapshots of consumption with LDAP/JWT protected API access.</p>
<h1 class="web2-page-title">Chargeback Intelligence Dashboard</h1>
<p class="web2-page-subtitle">Point-in-time snapshots of vSphere consumption with LDAP and JWT-protected API access.</p>
</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/daily">Daily 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="/vcenters">vCenters</a>
<a class="web2-button" href="/swagger/">Swagger UI</a>
<a class="web2-button secondary" href="/vcenters">vCenters</a>
<a class="web2-button secondary" href="/swagger/">Swagger UI</a>
</div>
</div>
</section>
<section class="grid gap-6 md:grid-cols-3">
<div class="web2-card">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Build Time</p>
<p class="mt-3 text-xl font-semibold">{info.BuildTime}</p>
</div>
<div class="web2-card">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">SHA1 Version</p>
<p class="mt-3 text-xl font-semibold">{info.SHA1Ver}</p>
</div>
<div class="web2-card">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Go Runtime</p>
<p class="mt-3 text-xl font-semibold">{info.GoVersion}</p>
<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 class="web2-kpi-grid">
<div class="web2-card">
<p class="web2-kpi-label">Build Time</p>
<p class="web2-kpi-value">{ info.BuildTime }</p>
</div>
<div class="web2-card">
<p class="web2-kpi-label">SHA1 Version</p>
<p class="web2-kpi-value">{ info.SHA1Ver }</p>
</div>
<div class="web2-card">
<p class="web2-kpi-label">Go Runtime</p>
<p class="web2-kpi-value">{ info.GoVersion }</p>
</div>
</section>
<section class="grid gap-6 lg:grid-cols-3">
<div class="web2-card">
<h2 class="text-lg font-semibold mb-2">Overview</h2>
<p class="mt-2 text-sm text-slate-600">
<h2 class="mb-2">Overview</h2>
<p class="web2-page-subtitle">
vCTP is a vSphere Chargeback Tracking Platform.
</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.
</p>
<p class="mt-2 text-sm text-slate-600">
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>.
</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 class="web2-page-subtitle">
Use <code class="web2-code">/api/auth/me</code> to inspect active claims and roles during integration and diagnostics.
</p>
</div>
<div class="web2-card">
<h2 class="text-lg font-semibold mb-2">Snapshots and Reports</h2>
<div class="mt-3 text-sm text-slate-600 web2-paragraphs">
<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>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>Monthly tracks:</strong> the same daily aggregate fields, with monthly values weighted by per-day sample volume so partial-day VMs and config changes stay proportional.</p>
<p>Snapshots are registered in <code class="web2-code">snapshot_registry</code> so regeneration via <code class="web2-code">/api/snapshots/aggregate</code> can locate the correct tables (fallback scanning is also supported).</p>
<p>vCenter totals pages are accelerated by compact cache tables: <code class="web2-code">vcenter_latest_totals</code> and <code class="web2-code">vcenter_aggregate_totals</code>.</p>
<p>VM Trace daily mode uses the <code class="web2-code">vm_daily_rollup</code> cache when available, and falls back to daily summary tables if needed.</p>
<p>Reports (XLSX with totals/charts) are generated automatically after hourly, daily, and monthly jobs and written to a reports directory.</p>
<p>Hourly totals are interval-based: each row represents <code class="web2-code">[HH:00, HH+1:00)</code> and uses the first snapshot at or after the hour end (including cross-day snapshots) to prorate VM presence.</p>
<p>Monthly aggregation reports include a Daily Totals sheet with full-day interval labels (YYYY-MM-DD to YYYY-MM-DD) and prorated totals.</p>
</div>
<div class="web2-card">
<h2 class="mb-2">Snapshots and Reports</h2>
<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><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>Monthly tracks:</strong> the same daily aggregate fields, with monthly values weighted by per-day sample volume so partial-day VMs and config changes stay proportional.</p>
<p>Snapshots are registered in <code class="web2-code">snapshot_registry</code> so regeneration via <code class="web2-code">/api/snapshots/aggregate</code> can locate the correct tables (fallback scanning is also supported).</p>
<p>vCenter totals pages are accelerated by compact cache tables: <code class="web2-code">vcenter_latest_totals</code> and <code class="web2-code">vcenter_aggregate_totals</code>.</p>
<p>VM Trace daily mode uses the <code class="web2-code">vm_daily_rollup</code> cache when available, and falls back to daily summary tables if needed.</p>
<p>Reports (XLSX with totals/charts) are generated automatically after hourly, daily, and monthly jobs and written to a reports directory.</p>
<p>Hourly totals are interval-based: each row represents <code class="web2-code">[HH:00, HH+1:00)</code> and uses the first snapshot at or after the hour end (including cross-day snapshots) to prorate VM presence.</p>
<p>Monthly aggregation reports include a Daily Totals sheet with full-day interval labels (YYYY-MM-DD to YYYY-MM-DD) and prorated totals.</p>
</div>
<div class="web2-card">
<h2 class="text-lg font-semibold mb-2">Prorating and Aggregation</h2>
<div class="mt-3 space-y-2 text-sm text-slate-600 web2-paragraphs">
<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>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 pool percentages (<code class="web2-code">PoolTinPct</code>/<code class="web2-code">PoolBronzePct</code>/<code class="web2-code">PoolSilverPct</code>/<code class="web2-code">PoolGoldPct</code>) use pool-hit counts divided by <code class="web2-code">SamplesPresent</code>.</p>
<p>Monthly aggregation converts each day into weighted sums using sample volume, then recomputes monthly averages and pool percentages from those weighted totals.</p>
<p>CreationTime is only set when vCenter provides it; otherwise it remains 0.</p>
</div>
</div>
<div class="web2-card">
<h2 class="mb-2">Prorating and Aggregation</h2>
<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">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 pool percentages (<code class="web2-code">PoolTinPct</code>/<code class="web2-code">PoolBronzePct</code>/<code class="web2-code">PoolSilverPct</code>/<code class="web2-code">PoolGoldPct</code>) use pool-hit counts divided by <code class="web2-code">SamplesPresent</code>.</p>
<p>Monthly aggregation converts each day into weighted sums using sample volume, then recomputes monthly averages and pool percentages from those weighted totals.</p>
<p>CreationTime is only set when vCenter provides it; otherwise it remains 0.</p>
</div>
</section>
</div>
</section>
</main>
</body>
@core.Footer()
File diff suppressed because one or more lines are too long
+75 -57
View File
@@ -1,6 +1,9 @@
package views
import "vctp/components/core"
import (
"fmt"
"vctp/components/core"
)
type SnapshotEntry struct {
Label string
@@ -52,23 +55,24 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
<html lang="en">
@core.Header()
<body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell space-y-8">
<section class="web2-header">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div class="web2-pill">Snapshot Library</div>
<h1 class="mt-3 text-4xl font-bold">{ title }</h1>
<p class="mt-2 text-sm text-slate-600">{ subtitle }</p>
</div>
<a class="web2-button" href="/">Back to Dashboard</a>
</div>
<main class="flex-grow web2-shell web2-card-grid">
<section class="web2-header web2-page-head">
@core.PageHeader(
"Snapshot Library",
title,
subtitle,
[]core.ActionLink{
{
Label: "Back to Dashboard",
Href: "/",
Class: "web2-button secondary",
},
},
)
</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">{ len(entries) } files</span>
</div>
<div class="overflow-hidden border border-slate-200 rounded">
@core.SectionHead("Available Exports", fmt.Sprintf("%d files", len(entries)))
<div class="web2-table-shell">
<table class="web2-table">
<thead>
<tr>
@@ -81,13 +85,13 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
for i, entry := range entries {
if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) {
<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>
<td>
<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>
</td>
<td>
@@ -113,23 +117,24 @@ templ VcenterList(links []VcenterLink) {
<html lang="en">
@core.Header()
<body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell space-y-8">
<section class="web2-header">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div class="web2-pill">vCenter Inventory</div>
<h1 class="mt-3 text-4xl font-bold">Monitored vCenters</h1>
<p class="mt-2 text-sm text-slate-600">Select a vCenter to view snapshot totals over time.</p>
</div>
<a class="web2-button" href="/">Back to Dashboard</a>
</div>
<main class="flex-grow web2-shell web2-card-grid">
<section class="web2-header web2-page-head">
@core.PageHeader(
"vCenter Inventory",
"Monitored vCenters",
"Select a vCenter to view snapshot totals over time.",
[]core.ActionLink{
{
Label: "Back to Dashboard",
Href: "/",
Class: "web2-button secondary",
},
},
)
</section>
<section class="web2-card">
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
<h2 class="text-lg font-semibold">vCenters</h2>
<span class="web2-badge">{ len(links) } total</span>
</div>
<div class="overflow-hidden border border-slate-200 rounded">
@core.SectionHead("vCenters", fmt.Sprintf("%d total", len(links)))
<div class="web2-table-shell">
<table class="web2-table">
<thead>
<tr>
@@ -140,7 +145,7 @@ templ VcenterList(links []VcenterLink) {
<tbody>
for _, link := range links {
<tr>
<td class="font-semibold text-slate-700">{ link.Name }</td>
<td class="font-semibold">{ link.Name }</td>
<td class="text-right">
<a class="web2-link" href={ link.Link }>View Totals</a>
</td>
@@ -161,29 +166,42 @@ templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcen
<html lang="en">
@core.Header()
<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 { vcenter }</h1>
<p class="mt-2 text-sm text-slate-600">{ meta.TypeLabel } snapshots of VM count, vCPU, and RAM over time.</p>
</div>
<div class="flex gap-3">
<a class="web2-button secondary" href="/vcenters">All vCenters</a>
<a class="web2-button" href="/">Dashboard</a>
</div>
</div>
<div class="web3-button-group mt-8 mb-3">
<a class={ meta.HourlyClass } href={ meta.HourlyLink }>Hourly Detail (45d)</a>
<a class={ meta.DailyClass } href={ meta.DailyLink }>Daily Aggregated</a>
</div>
</section>
<main class="flex-grow web2-shell web2-shell-wide web2-card-grid">
<section class="web2-header web2-page-head">
@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",
},
},
)
@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 class="web2-card">
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
<h2 class="text-lg font-semibold">{ meta.TypeLabel } Snapshots</h2>
<span class="web2-badge">{ len(entries) } records</span>
</div>
@core.SectionHead(meta.TypeLabel+" Snapshots", fmt.Sprintf("%d records", len(entries)))
if chart.ConfigJSON != "" {
<div class="mb-6 overflow-auto">
<div class="web3-chart-frame">
@@ -198,7 +216,7 @@ templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcen
</script>
</div>
}
<div class="overflow-hidden border border-slate-200 rounded">
<div class="web2-table-shell">
<table class="web2-table">
<thead>
<tr>
+147 -219
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
// templ: version: v0.3.1001
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -8,7 +8,10 @@ package views
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "vctp/components/core"
import (
"fmt"
"vctp/components/core"
)
type SnapshotEntry struct {
Label string
@@ -159,114 +162,102 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 60, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
templ_7745c5c3_Err = core.PageHeader(
"Snapshot Library",
title,
subtitle,
[]core.ActionLink{
{
Label: "Back to Dashboard",
Href: "/",
Class: "web2-button secondary",
},
},
).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 61, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
templ_7745c5c3_Err = core.SectionHead("Available Exports", fmt.Sprintf("%d files", len(entries))).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</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\">")
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>")
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
}
for i, entry := range entries {
if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group)
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 84, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 88, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</td></tr>")
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, 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, 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_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 90, Col: 76}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 94, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span></div></td><td><span class=\"web2-badge\">")
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_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count)
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 94, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 98, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " records</span></td><td class=\"text-right\"><a class=\"web2-link\" href=\"")
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_Var11 templ.SafeURL
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
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: 97, Col: 49}
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_Var11))
_, 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, 11, "\">Download XLSX</a></td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">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>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</tbody></table></div></section></main></body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -274,7 +265,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -298,12 +289,12 @@ func VcenterList(links []VcenterLink) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil {
templ_7745c5c3_Var12 = templ.NopComponent
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<!doctype html><html lang=\"en\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -311,34 +302,48 @@ func VcenterList(links []VcenterLink) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(len(links))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 130, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
templ_7745c5c3_Err = core.PageHeader(
"vCenter Inventory",
"Monitored vCenters",
"Select a vCenter to view snapshot totals over time.",
[]core.ActionLink{
{
Label: "Back to Dashboard",
Href: "/",
Class: "web2-button secondary",
},
},
).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " total</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 {
return templ_7745c5c3_Err
}
for _, link := range links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<tr><td class=\"font-semibold text-slate-700\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<tr><td class=\"font-semibold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name)
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 143, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 148, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -346,12 +351,12 @@ func VcenterList(links []VcenterLink) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 templ.SafeURL
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link)
var templ_7745c5c3_Var11 templ.SafeURL
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 145, Col: 48}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 150, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -392,9 +397,9 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
if templ_7745c5c3_Var16 == nil {
templ_7745c5c3_Var16 = templ.NopComponent
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil {
templ_7745c5c3_Var12 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<!doctype html><html lang=\"en\">")
@@ -405,214 +410,137 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<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
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vcenter)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 169, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
templ_7745c5c3_Err = core.PageHeader(
"vCenter Totals",
"Totals for "+vcenter,
meta.TypeLabel+" snapshots of VM count, vCPU, and RAM over time.",
[]core.ActionLink{
{
Label: "All vCenters",
Href: "/vcenters",
Class: "web2-button secondary",
},
{
Label: "Dashboard",
Href: "/",
Class: "web2-button",
},
},
).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</h1><p class=\"mt-2 text-sm text-slate-600\">")
templ_7745c5c3_Err = core.SegmentedActions(
[]core.SegmentedLink{
{
Label: "Hourly Detail (45d)",
Href: meta.HourlyLink,
Class: meta.HourlyClass,
},
{
Label: "Daily Aggregated",
Href: meta.DailyLink,
Class: meta.DailyClass,
},
},
).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 170, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</section><section class=\"web2-card\">")
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>")
templ_7745c5c3_Err = core.SectionHead(meta.TypeLabel+" Snapshots", fmt.Sprintf("%d records", len(entries))).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if chart.ConfigJSON != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<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=\"")
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_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON)
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: 190, Col: 145}
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_Var27))
_, 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, 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>")
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, 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, 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, 37, "<tr><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<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)
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 214, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 232, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</td><td class=\"text-right\">")
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_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount)
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 215, Col: 48}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 233, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</td><td class=\"text-right\">")
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_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal)
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 216, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 234, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</td><td class=\"text-right\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</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)
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 235, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</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>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</tbody></table></div></section></main></body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -620,7 +548,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+87 -79
View File
@@ -6,18 +6,18 @@ import (
)
type VmTraceEntry struct {
Snapshot string
RawTime int64
Name string
VmId string
VmUuid string
Vcenter string
ResourcePool string
VcpuCount int64
RamGB int64
Snapshot string
RawTime int64
Name string
VmId string
VmUuid string
Vcenter string
ResourcePool string
VcpuCount int64
RamGB int64
ProvisionedDisk float64
CreationTime string
DeletionTime string
CreationTime string
DeletionTime string
}
type VmTraceChart struct {
@@ -25,12 +25,12 @@ type VmTraceChart struct {
}
type VmTraceMeta struct {
ViewType string
TypeLabel string
HourlyLink string
DailyLink string
ViewType string
TypeLabel string
HourlyLink string
DailyLink string
HourlyClass string
DailyClass string
DailyClass string
}
type VmTraceDiagnosticLine struct {
@@ -40,7 +40,7 @@ type VmTraceDiagnosticLine struct {
type VmTraceDiagnostics struct {
Visible bool
Lines []VmTraceDiagnosticLine
Lines []VmTraceDiagnosticLine
}
templ VmTracePage(query string, display_query string, vm_id string, vm_uuid string, vm_name string, creationLabel string, deletionLabel string, creationApprox bool, entries []VmTraceEntry, chart VmTraceChart, meta VmTraceMeta, diagnostics VmTraceDiagnostics) {
@@ -48,52 +48,60 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri
<html lang="en">
@core.Header()
<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{display_query}</h1>
<p class="mt-2 text-sm text-slate-600">Timeline of vCPU, RAM, and resource pool changes across { meta.TypeLabel } snapshots.</p>
</div>
<div class="flex gap-3 flex-wrap">
<a class="web2-button" href="/">Dashboard</a>
</div>
<main class="flex-grow web2-shell web2-shell-wide web2-card-grid">
<section class="web2-header web2-page-head">
@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",
},
},
)
<form method="get" action="/vm/trace" class="web2-form-grid">
<input type="hidden" name="view" value={ meta.ViewType }/>
<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={ vm_id } placeholder="vm-12345"/>
</div>
<form method="get" action="/vm/trace" class="mt-4 grid gap-3 md:grid-cols-3">
<input type="hidden" name="view" value={ meta.ViewType }/>
<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={vm_id} 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={vm_uuid} 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={vm_name} 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">
<a class={ meta.HourlyClass } href={ meta.HourlyLink }>Hourly Detail</a>
<a class={ meta.DailyClass } href={ meta.DailyLink }>Daily Aggregated</a>
<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={ vm_uuid } placeholder="uuid..."/>
</div>
</section>
<div class="web2-field">
<label class="web2-label" for="name">Name</label>
<input class="web2-input" type="text" id="name" name="name" value={ vm_name } 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>
@core.SegmentedActions(
[]core.SegmentedLink{
{
Label: "Hourly Detail",
Href: meta.HourlyLink,
Class: meta.HourlyClass,
},
{
Label: "Daily Aggregated",
Href: meta.DailyLink,
Class: meta.DailyClass,
},
},
)
</section>
<section class="web2-card">
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
<h2 class="text-lg font-semibold">{ meta.TypeLabel } Timeline</h2>
<span class="web2-badge">{len(entries)} samples</span>
</div>
@core.SectionHead(meta.TypeLabel+" Timeline", fmt.Sprintf("%d samples", len(entries)))
if chart.ConfigJSON != "" {
<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={chart.ConfigJSON}></canvas>
<canvas id="vm-trace-chart" class="web3-chart-canvas" role="img" aria-label="VM timeline" data-chart-config={ chart.ConfigJSON }></canvas>
<div id="vm-trace-tooltip" class="web3-chart-tooltip" aria-hidden="true"></div>
</div>
<script>
@@ -105,28 +113,28 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri
</div>
}
<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">{creationLabel}</p>
<div class="web2-subcard">
<p class="web2-subcard-label">Creation Time</p>
<p class="web2-subcard-value">{ creationLabel }</p>
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 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">{deletionLabel}</p>
<div class="web2-subcard">
<p class="web2-subcard-label">Deletion Time</p>
<p class="web2-subcard-value">{ deletionLabel }</p>
</div>
</div>
if diagnostics.Visible && len(diagnostics.Lines) > 0 {
<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">
<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>
for _, line := range diagnostics.Lines {
<tr>
<td class="font-semibold text-slate-700 w-72">{ line.Label }</td>
<td class="text-slate-600">{ line.Value }</td>
<td class="font-semibold w-72">{ line.Label }</td>
<td class="web2-muted">{ line.Value }</td>
</tr>
}
</tbody>
@@ -134,7 +142,7 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri
</div>
</details>
}
<div class="overflow-hidden border border-slate-200 rounded">
<div class="web2-table-shell">
<table class="web2-table">
<thead>
<tr>
@@ -152,15 +160,15 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri
<tbody>
for _, e := range entries {
<tr>
<td>{e.Snapshot}</td>
<td>{e.Name}</td>
<td>{e.VmId}</td>
<td>{e.VmUuid}</td>
<td>{e.Vcenter}</td>
<td>{e.ResourcePool}</td>
<td class="text-right">{e.VcpuCount}</td>
<td class="text-right">{e.RamGB}</td>
<td class="text-right">{fmt.Sprintf("%.1f", e.ProvisionedDisk)}</td>
<td>{ e.Snapshot }</td>
<td>{ e.Name }</td>
<td>{ e.VmId }</td>
<td>{ e.VmUuid }</td>
<td>{ e.Vcenter }</td>
<td>{ e.ResourcePool }</td>
<td class="text-right">{ e.VcpuCount }</td>
<td class="text-right">{ e.RamGB }</td>
<td class="text-right">{ fmt.Sprintf("%.1f", e.ProvisionedDisk) }</td>
</tr>
}
</tbody>
+246 -324
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
// templ: version: v0.3.1001
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -80,413 +80,335 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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 {
return templ_7745c5c3_Err
}
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 {
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))
if templ_7745c5c3_Err != nil {
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 {
return templ_7745c5c3_Err
}
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 {
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))
if templ_7745c5c3_Err != nil {
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 {
return templ_7745c5c3_Err
}
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 {
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))
if templ_7745c5c3_Err != nil {
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 {
return templ_7745c5c3_Err
}
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 {
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))
if templ_7745c5c3_Err != nil {
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
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(vm_uuid)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 71, Col: 129}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
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, 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, 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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON)
if templ_7745c5c3_Err != nil {
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))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(vm_name)
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(creationLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 75, Col: 123}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 118, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" placeholder=\"VM name\"></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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 = []any{meta.HourlyClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if creationApprox {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"web2-muted web2-caption mt-1\">Approximate (earliest snapshot)</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
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
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a class=\"")
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
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" href=\"")
if diagnostics.Visible && len(diagnostics.Lines) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(line.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 136, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td class=\"web2-muted\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(line.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 137, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</tbody></table></div></details>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
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
}
var templ_7745c5c3_Var10 templ.SafeURL
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(meta.HourlyLink)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 83, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">Hourly Detail</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 = []any{meta.DailyClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(meta.DailyLink)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 84, Col: 57}
}
_, 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, 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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 90, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " Timeline</h2><span class=\"web2-badge\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 91, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " samples</span></div>")
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=\"")
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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 164, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmId)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 165, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmUuid)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 166, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vcenter)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 167, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON)
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(e.ResourcePool)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 96, Col: 133}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 168, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"></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
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(e.VcpuCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 169, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(e.RamGB)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 170, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", e.ProvisionedDisk))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 171, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td></tr>")
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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(creationLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 110, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</p>")
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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(deletionLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 117, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</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, 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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(line.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 128, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td><td class=\"text-slate-600\">")
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 {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</tbody></table></div></details>")
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>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</tbody></table></div></section></main></body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -494,7 +416,7 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</html>")
if templ_7745c5c3_Err != nil {
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 {
--web2-blue: #1d9bf0;
--web2-slate: #0f172a;
--web2-muted: #64748b;
--web2-card: #ffffff;
--web2-border: #e5e7eb;
--theme_text_primary: #181d26;
--theme_text_weak: rgba(4, 14, 32, 0.69);
--theme_text_inverse: rgba(249, 252, 255, 0.97);
--theme_text_secondary_active: rgba(7, 12, 20, 0.82);
--theme_accent_blue: #1b61c9;
--theme_accent_blue_hover: #164fa6;
--theme_accent_blue_soft: #e8f0fc;
--theme_success_text: #006400;
--theme_surface_primary: #ffffff;
--theme_surface_subtle: #f8fafc;
--theme_surface_shell: #fdfefe;
--theme_surface_section: #fbfdff;
--theme_border: #e0e2e6;
--theme_shadow_card: rgba(0, 0, 0, 0.32) 0 0 1px, rgba(0, 0, 0, 0.08) 0 0 2px, rgba(45, 127, 249, 0.28) 0 1px 3px, rgba(0, 0, 0, 0.06) 0 0 0 0.5px inset;
--theme_shadow_soft: rgba(15, 48, 106, 0.05) 0 0 20px;
--theme_shadow_button: rgba(45, 127, 249, 0.28) 0 1px 3px;
--theme_radius_button: 12px;
--theme_radius_card: 16px;
--theme_radius_section: 24px;
--theme_radius_large: 32px;
--theme_font_body: "Haas", "Neue Haas Grotesk Text Pro", "Avenir Next", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
--theme_font_display: "Haas Groot Disp", "Haas", "Neue Haas Grotesk Display Pro", "Avenir Next", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
--theme_font_code: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--theme_letter_body: 0.12px;
--theme_letter_caption: 0.2px;
--theme_letter_button: 0.08px;
--theme_transition_fast: 150ms ease;
--theme_transition_base: 220ms ease;
--web2-blue: var(--theme_accent_blue);
--web2-slate: var(--theme_text_primary);
--web2-muted: var(--theme_text_weak);
--web2-card: var(--theme_surface_primary);
--web2-border: var(--theme_border);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
color: var(--web2-slate);
margin: 0;
padding: 0;
}
body {
font-family: var(--theme_font_body);
color: var(--theme_text_primary);
letter-spacing: var(--theme_letter_body);
background: var(--theme_surface_shell);
}
.web2-bg {
background: #ffffff;
background:
radial-gradient(circle at 8% 4%, rgba(27, 97, 201, 0.08) 0, rgba(27, 97, 201, 0) 30%),
radial-gradient(circle at 90% 12%, rgba(37, 79, 173, 0.08) 0, rgba(37, 79, 173, 0) 28%),
var(--theme_surface_shell);
}
.web2-shell {
max-width: 1100px;
max-width: 1140px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
padding: 2.5rem 1.5rem 2.25rem;
}
.web2-shell-wide {
max-width: 1400px;
max-width: 1420px;
}
.web2-page-head {
display: flex;
flex-direction: column;
gap: 1rem;
}
.web2-page-head-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.web2-head-copy {
max-width: 72ch;
}
.web2-page-title {
margin-top: 0.6rem;
font-family: var(--theme_font_display);
font-size: clamp(1.95rem, 1.2rem + 1.9vw, 2.65rem);
line-height: 1.15;
letter-spacing: 0.06px;
}
.web2-page-subtitle {
margin-top: 0.45rem;
font-size: 0.96rem;
line-height: 1.45;
color: var(--theme_text_weak);
}
.web2-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.web2-section-head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 1rem;
}
.web2-kpi-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.web2-kpi-label {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.22em;
color: var(--theme_text_weak);
}
.web2-kpi-value {
margin-top: 0.55rem;
font-size: 1.3rem;
font-weight: 600;
line-height: 1.2;
color: var(--theme_text_primary);
}
.web2-note {
padding: 0.78rem 0.92rem;
border-radius: var(--theme_radius_button);
border: 1px solid rgba(27, 97, 201, 0.28);
background: rgba(27, 97, 201, 0.08);
color: var(--theme_text_secondary_active);
font-size: 0.89rem;
line-height: 1.45;
}
.web2-header {
background: var(--web2-card);
border: 1px solid var(--web2-border);
border-radius: 4px;
padding: 1.5rem 2rem;
background: var(--theme_surface_section);
border: 1px solid var(--theme_border);
border-radius: var(--theme_radius_section);
padding: 1.65rem 2rem;
box-shadow: var(--theme_shadow_card), var(--theme_shadow_soft);
}
.web2-card {
background: var(--web2-card);
border: 1px solid var(--web2-border);
border-radius: 4px;
padding: 1.5rem 1.75rem;
background: var(--theme_surface_primary);
border: 1px solid var(--theme_border);
border-radius: var(--theme_radius_card);
padding: 1.4rem 1.6rem;
box-shadow: var(--theme_shadow_card);
}
.web2-card h2 {
position: relative;
padding-left: 0.75rem;
font-size: 1.05rem;
font-weight: 700;
letter-spacing: 0.02em;
color: #0b1220;
padding-left: 0.9rem;
font-size: 1.1rem;
font-family: var(--theme_font_display);
font-weight: 600;
letter-spacing: var(--theme_letter_button);
color: var(--theme_text_primary);
}
.web2-card h2::before {
content: "";
position: absolute;
@@ -47,191 +184,355 @@ body {
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 70%;
background: var(--web2-blue);
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(29, 155, 240, 0.18);
height: 72%;
background: var(--theme_accent_blue);
border-radius: 999px;
box-shadow: 0 0 0 1px rgba(27, 97, 201, 0.22);
}
.web2-pill {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: #f8fafc;
border: 1px solid var(--web2-border);
color: var(--web2-muted);
padding: 0.2rem 0.6rem;
border-radius: 3px;
font-size: 0.85rem;
letter-spacing: 0.02em;
background: var(--theme_surface_subtle);
border: 1px solid var(--theme_border);
color: var(--theme_text_weak);
padding: 0.28rem 0.72rem;
border-radius: var(--theme_radius_button);
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.24px;
}
.web2-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
background: #f1f5f9;
border: 1px solid var(--web2-border);
border-radius: 3px;
padding: 0.1rem 0.35rem;
font-size: 0.85em;
color: #0f172a;
font-family: var(--theme_font_code);
background: rgba(27, 97, 201, 0.07);
border: 1px solid var(--theme_border);
border-radius: 8px;
padding: 0.12rem 0.42rem;
font-size: 0.84em;
color: var(--theme_text_primary);
}
.web2-paragraphs p + p {
margin-top: 0.85rem;
}
.web2-link {
color: var(--web2-blue);
color: var(--theme_accent_blue);
text-decoration: none;
font-weight: 600;
transition: color var(--theme_transition_fast), text-decoration-color var(--theme_transition_fast);
}
.web2-link:hover {
color: var(--theme_accent_blue_hover);
text-decoration: underline;
}
.web2-button {
background: var(--web2-blue);
color: #fff;
padding: 0.45rem 0.9rem;
border-radius: 3px;
border: 1px solid #1482d0;
box-shadow: none;
background: var(--theme_accent_blue);
color: var(--theme_text_inverse);
padding: 0.56rem 1rem;
border-radius: var(--theme_radius_button);
border: 1px solid rgba(22, 79, 166, 0.95);
box-shadow: var(--theme_shadow_button);
font-weight: 600;
letter-spacing: var(--theme_letter_button);
text-decoration: none;
transition: transform var(--theme_transition_fast), background var(--theme_transition_fast), border-color var(--theme_transition_fast), box-shadow var(--theme_transition_fast);
}
.web2-button:hover {
background: #1787d4;
background: var(--theme_accent_blue_hover);
border-color: var(--theme_accent_blue_hover);
box-shadow: rgba(37, 79, 173, 0.32) 0 3px 7px;
transform: translateY(-1px);
}
.web2-button.secondary {
background: var(--theme_surface_primary);
color: var(--theme_text_secondary_active);
border-color: var(--theme_border);
box-shadow: none;
}
.web2-button.secondary:hover {
background: var(--theme_surface_subtle);
border-color: #c7cdd6;
color: var(--theme_text_primary);
transform: none;
}
.web2-button-group {
display: flex;
flex-wrap: wrap;
}
.web2-button-group .web2-button {
margin: 0 0.5rem 0.5rem 0;
}
.web3-button {
background: #f3f4f6;
color: #0f172a;
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #e5e7eb;
background: var(--theme_surface_subtle);
color: var(--theme_text_primary);
padding: 0.52rem 1.02rem;
border-radius: var(--theme_radius_button);
border: 1px solid var(--theme_border);
text-decoration: none;
font-weight: 600;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
letter-spacing: var(--theme_letter_button);
transition: background var(--theme_transition_fast), border-color var(--theme_transition_fast), color var(--theme_transition_fast), box-shadow var(--theme_transition_fast);
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.web3-button:hover {
background: #e2e8f0;
border-color: #cbd5e1;
background: #edf2fa;
border-color: #c8d2e1;
}
.web3-button.active {
background: #dbeafe;
border-color: #93c5fd;
color: #1d4ed8;
box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.35);
background: var(--theme_accent_blue_soft);
border-color: rgba(27, 97, 201, 0.45);
color: var(--theme_accent_blue);
box-shadow: rgba(45, 127, 249, 0.28) 0 0 0 2px;
}
.web3-button-group {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-top: 4px;
}
.web2-list li {
background: #ffffff;
border: 1px solid var(--web2-border);
border-radius: 3px;
padding: 0.75rem 1rem;
box-shadow: none;
.web2-table-shell {
overflow-x: auto;
overflow-y: hidden;
border: 1px solid var(--theme_border);
border-radius: var(--theme_radius_card);
background: var(--theme_surface_primary);
}
.web2-list li {
background: var(--theme_surface_primary);
border: 1px solid var(--theme_border);
border-radius: var(--theme_radius_card);
padding: 0.75rem 1rem;
box-shadow: var(--theme_shadow_card);
}
.web2-table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
letter-spacing: 0.08px;
min-width: 560px;
}
.web2-table thead th {
text-align: left;
padding: 0.75rem 0.5rem;
padding: 0.8rem 0.55rem;
font-weight: 700;
color: var(--web2-muted);
border-bottom: 1px solid var(--web2-border);
color: var(--theme_text_weak);
letter-spacing: var(--theme_letter_caption);
border-bottom: 1px solid var(--theme_border);
background: rgba(248, 250, 252, 0.95);
}
.web2-table tbody td {
padding: 0.9rem 0.5rem;
border-bottom: 1px solid var(--web2-border);
padding: 0.92rem 0.55rem;
border-bottom: 1px solid var(--theme_border);
}
.web2-table tbody tr:nth-child(odd) {
background: #f8fafc;
background: var(--theme_surface_subtle);
}
.web2-table tbody tr:nth-child(even) {
background: #ffffff;
background: var(--theme_surface_primary);
}
.web2-group-row td {
background: #e8eef5;
color: #0f172a;
border-bottom: 1px solid var(--web2-border);
padding: 0.65rem 0.5rem;
background: #eaf1fb;
color: var(--theme_text_primary);
border-bottom: 1px solid var(--theme_border);
padding: 0.7rem 0.55rem;
}
.web2-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
border: 1px solid var(--web2-border);
padding: 0.15rem 0.45rem;
border-radius: 3px;
border: 1px solid var(--theme_border);
padding: 0.16rem 0.5rem;
border-radius: var(--theme_radius_button);
font-size: 0.8rem;
color: var(--web2-muted);
background: #f8fafc;
letter-spacing: var(--theme_letter_caption);
color: var(--theme_text_weak);
background: var(--theme_surface_subtle);
}
.web2-form-grid {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.web2-field {
display: flex;
flex-direction: column;
gap: 0.32rem;
}
.web2-label {
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.1px;
color: var(--theme_text_weak);
}
.web2-input {
width: 100%;
padding: 0.62rem 0.72rem;
border: 1px solid var(--theme_border);
border-radius: var(--theme_radius_button);
background: var(--theme_surface_primary);
color: var(--theme_text_primary);
font: inherit;
letter-spacing: var(--theme_letter_body);
transition: border-color var(--theme_transition_fast), box-shadow var(--theme_transition_fast), background var(--theme_transition_fast);
}
.web2-input::placeholder {
color: rgba(4, 14, 32, 0.46);
}
.web2-input:hover {
border-color: #c9d0db;
}
.web2-input:focus-visible {
outline: none;
border-color: rgba(27, 97, 201, 0.8);
box-shadow: rgba(45, 127, 249, 0.26) 0 0 0 3px;
}
.web2-form-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.web2-form-actions-full {
grid-column: 1 / -1;
}
.web2-subcard {
padding: 0.95rem 1rem;
border-radius: var(--theme_radius_card);
border: 1px solid var(--theme_border);
background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0, rgba(248, 250, 252, 0.75) 100%);
}
.web2-subcard-label {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--theme_text_weak);
}
.web2-subcard-value {
margin-top: 0.45rem;
font-size: 1rem;
font-weight: 600;
color: var(--theme_text_primary);
}
.web2-muted {
color: var(--theme_text_weak);
}
.web2-caption {
font-size: 0.76rem;
letter-spacing: var(--theme_letter_caption);
}
.web2-details-summary {
cursor: pointer;
font-size: 0.86rem;
font-weight: 600;
color: var(--theme_text_secondary_active);
}
.web2-card-grid {
display: grid;
gap: 1.4rem;
}
.web3-chart-frame {
position: relative;
min-width: 760px;
width: 100%;
}
.web3-chart-canvas {
display: block;
width: 100%;
height: 360px;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: var(--theme_surface_primary);
border: 1px solid var(--theme_border);
border-radius: var(--theme_radius_card);
}
.web3-chart-tooltip {
position: absolute;
left: 0;
top: 0;
opacity: 0;
pointer-events: none;
background: rgba(15, 23, 42, 0.95);
color: #f8fafc;
background: rgba(24, 29, 38, 0.97);
color: var(--theme_text_inverse);
padding: 0.55rem 0.65rem;
border-radius: 6px;
border-radius: var(--theme_radius_button);
font-size: 0.75rem;
line-height: 1.35;
min-width: 170px;
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.25);
z-index: 20;
transition: opacity 0.08s linear;
transition: opacity 80ms linear;
}
.web3-chart-tooltip.visible {
opacity: 1;
}
.web3-chart-tooltip-title {
font-weight: 700;
color: #e2e8f0;
margin-bottom: 0.35rem;
}
.web3-chart-tooltip-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.65rem;
}
.web3-chart-tooltip-label {
display: inline-flex;
align-items: center;
color: #cbd5e1;
}
.web3-chart-tooltip-value {
font-weight: 700;
color: #f8fafc;
}
.web3-chart-tooltip-swatch {
display: inline-block;
width: 8px;
@@ -239,8 +540,74 @@ body {
border-radius: 999px;
margin-right: 0.35rem;
}
.web2-footer {
width: 100%;
padding: 0.65rem 1.5rem 1.25rem;
}
.web2-footer-inner {
max-width: 1140px;
margin: 0 auto;
border-top: 1px solid var(--theme_border);
padding-top: 0.75rem;
text-align: center;
font-size: 0.74rem;
font-style: italic;
letter-spacing: var(--theme_letter_caption);
color: var(--theme_text_weak);
}
a:focus-visible,
button:focus-visible,
summary:focus-visible,
.web2-button:focus-visible,
.web3-button:focus-visible,
.web2-link:focus-visible {
outline: 2px solid rgba(27, 97, 201, 0.7);
outline-offset: 2px;
}
@media (max-width: 900px) {
.web2-shell {
padding: 1.5rem 1rem 1.25rem;
}
.web2-header {
border-radius: var(--theme_radius_card);
padding: 1.2rem 1rem;
}
.web2-card {
padding: 1.1rem 1rem;
}
.web3-chart-frame {
min-width: 640px;
}
.web2-footer {
padding: 0.5rem 1rem 1rem;
}
.web2-page-title {
font-size: 1.82rem;
}
.web2-page-subtitle {
font-size: 0.9rem;
}
.web2-actions {
width: 100%;
}
.web2-actions .web2-button {
flex: 1 1 auto;
text-align: center;
}
.web2-table {
min-width: 520px;
}
}
@media (min-width: 780px) {
.web2-kpi-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.web2-form-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
+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.