Add a localhost-only web console so VM and image management no longer depends on the CLI for every inspection and lifecycle action. Wire bangerd up to a configurable web listener, expose dashboard and async image-build state through the daemon, and serve CSRF-protected HTML pages with host-path picking, VM/image detail views, logs, ports, and progress polling for long-running operations. Keep the browser path aligned with the existing sudo and host-owned artifact model: surface sudo readiness, print the web URL in daemon status, and document the new workflow. Polish the UI with resource usage cards, clearer clickable affordances, cancel paths, confirmation prompts, image-name links, and HTTP port links. Validation: GOCACHE=/tmp/banger-gocache go test ./...
124 lines
5 KiB
HTML
124 lines
5 KiB
HTML
{{define "page"}}
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{{.Title}} · banger</title>
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
</head>
|
|
<body>
|
|
<div class="app-shell">
|
|
<header class="topbar">
|
|
<div>
|
|
<p class="eyebrow">Local Control Plane</p>
|
|
<h1>banger</h1>
|
|
</div>
|
|
<nav class="nav">
|
|
<a href="/" class="{{if eq .Section "dashboard"}}active{{end}}">Dashboard</a>
|
|
<a href="/vms" class="{{if eq .Section "vms"}}active{{end}}">VMs</a>
|
|
<a href="/images" class="{{if eq .Section "images"}}active{{end}}">Images</a>
|
|
</nav>
|
|
</header>
|
|
|
|
{{if not .MutationAllowed}}
|
|
<section class="banner warning">
|
|
<strong>Mutating actions are paused.</strong>
|
|
<span>Run <code>{{.Summary.Sudo.Command}}</code> in a terminal and refresh this page. {{.Summary.Sudo.Error}}</span>
|
|
</section>
|
|
{{end}}
|
|
|
|
{{if .Flash}}
|
|
<section class="banner {{.Flash.Kind}}">
|
|
<span>{{.Flash.Message}}</span>
|
|
</section>
|
|
{{end}}
|
|
|
|
<section class="summary-grid">
|
|
<article class="summary-card resource-card cpu">
|
|
<div class="resource-head">
|
|
<h2>vCPU</h2>
|
|
<strong class="resource-ratio">{{.Summary.Banger.ConfiguredVCPUCount}} / {{.Summary.Host.CPUCount}}</strong>
|
|
</div>
|
|
<div class="resource-meter" aria-hidden="true">
|
|
<span class="resource-fill" style="width: {{percentOf .Summary.Banger.ConfiguredVCPUCount .Summary.Host.CPUCount}}%;"></span>
|
|
</div>
|
|
<div class="resource-foot">
|
|
<span>{{percentOf .Summary.Banger.ConfiguredVCPUCount .Summary.Host.CPUCount}}% allocated</span>
|
|
<span>{{.Summary.Banger.RunningVMCount}} running</span>
|
|
</div>
|
|
</article>
|
|
<article class="summary-card resource-card memory">
|
|
<div class="resource-head">
|
|
<h2>Memory</h2>
|
|
<strong class="resource-ratio">{{formatBytesCompact .Summary.Banger.ConfiguredMemoryBytes}} / {{formatBytesCompact .Summary.Host.TotalMemoryBytes}}</strong>
|
|
</div>
|
|
<div class="resource-meter" aria-hidden="true">
|
|
<span class="resource-fill" style="width: {{percentOf .Summary.Banger.ConfiguredMemoryBytes .Summary.Host.TotalMemoryBytes}}%;"></span>
|
|
</div>
|
|
<div class="resource-foot">
|
|
<span>{{percentOf .Summary.Banger.ConfiguredMemoryBytes .Summary.Host.TotalMemoryBytes}}% allocated</span>
|
|
<span>{{formatBytesCompact .Summary.Banger.RunningRSSBytes}} RSS live</span>
|
|
</div>
|
|
</article>
|
|
<article class="summary-card resource-card disk">
|
|
<div class="resource-head">
|
|
<h2>Disk</h2>
|
|
<strong class="resource-ratio">{{formatBytesCompact .Summary.Banger.ConfiguredDiskBytes}} / {{formatBytesCompact .Summary.Host.StateFilesystemTotalBytes}}</strong>
|
|
</div>
|
|
<div class="resource-meter" aria-hidden="true">
|
|
<span class="resource-fill" style="width: {{percentOf .Summary.Banger.ConfiguredDiskBytes .Summary.Host.StateFilesystemTotalBytes}}%;"></span>
|
|
</div>
|
|
<div class="resource-foot">
|
|
<span>{{formatBytesCompact .Summary.Host.StateFilesystemFreeBytes}} free</span>
|
|
<span>{{formatBytesCompact (sumInt64 .Summary.Banger.UsedSystemOverlayBytes .Summary.Banger.UsedWorkDiskBytes)}} actual</span>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
<div class="summary-notes">
|
|
<span>{{.Summary.Banger.RunningVMCount}} / {{.Summary.Banger.VMCount}} running</span>
|
|
<span>{{.Summary.Banger.ImageCount}} images</span>
|
|
<span>{{.Summary.Banger.ManagedImageCount}} managed</span>
|
|
<span>{{formatPercent .Summary.Banger.RunningCPUPercent}} live CPU</span>
|
|
</div>
|
|
|
|
<main class="content-panel">
|
|
<div class="panel-head">
|
|
<div><h2>{{.Title}}</h2></div>
|
|
</div>
|
|
{{.BodyHTML}}
|
|
</main>
|
|
</div>
|
|
|
|
<dialog class="picker-dialog" id="path-picker">
|
|
<form method="dialog" class="picker-shell">
|
|
<div class="picker-sidebar">
|
|
<h3>Roots</h3>
|
|
<div class="picker-roots">
|
|
{{range .PickerRoots}}
|
|
<button type="button" class="picker-root" data-picker-root="{{.Path}}">{{.Label}}</button>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
<div class="picker-main">
|
|
<div class="picker-bar">
|
|
<strong id="picker-current-path">/</strong>
|
|
<div class="picker-actions">
|
|
<button type="button" id="picker-select-current" class="secondary">Use current folder</button>
|
|
<button type="button" id="picker-close" class="secondary">Close</button>
|
|
</div>
|
|
</div>
|
|
<p class="picker-help">Choose a host path. Directories open in place; files select immediately.</p>
|
|
<div class="picker-list" id="picker-list"></div>
|
|
</div>
|
|
</form>
|
|
</dialog>
|
|
|
|
<script src="/static/app.js"></script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|
|
|
|
{{define "csrf_field"}}
|
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
|
{{end}}
|