Serve a local web UI from bangerd
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 ./...
This commit is contained in:
parent
30f0c0b54a
commit
2362d0ae39
24 changed files with 3308 additions and 52 deletions
124
internal/webui/templates/base.html
Normal file
124
internal/webui/templates/base.html
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
{{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}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue