banger/internal/webui/templates/base.html
Thales Maciel 2362d0ae39
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 ./...
2026-03-21 16:47:47 -03:00

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}}