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 ./...
191 lines
7.6 KiB
HTML
191 lines
7.6 KiB
HTML
{{define "vm_list_content"}}
|
|
<div class="section-head">
|
|
<p class="muted">Inspect lifecycle, capacity, and reachability for every VM.</p>
|
|
<a class="button" href="/vms/new">Create VM</a>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>State</th>
|
|
<th>Image</th>
|
|
<th>IP</th>
|
|
<th>vCPU</th>
|
|
<th>Memory</th>
|
|
<th>Disk</th>
|
|
<th>Created</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .VMs}}
|
|
<tr>
|
|
<td><a class="table-link" href="/vms/{{.ID}}">{{.Name}}</a></td>
|
|
<td><span class="state-pill {{stateClass .State}}">{{.State}}</span></td>
|
|
<td>{{$image := findImage $.Images .ImageID}}{{if $image.ID}}<a class="table-link" href="/images/{{$image.ID}}">{{$image.Name}}</a>{{else}}<code>{{shortID .ImageID}}</code>{{end}}</td>
|
|
<td>{{if .Runtime.GuestIP}}{{.Runtime.GuestIP}}{{else}}-{{end}}</td>
|
|
<td>{{.Spec.VCPUCount}}</td>
|
|
<td>{{.Spec.MemoryMiB}} MiB</td>
|
|
<td>{{formatBytes .Spec.WorkDiskSizeBytes}}</td>
|
|
<td>{{relativeTime .CreatedAt}}</td>
|
|
</tr>
|
|
{{else}}
|
|
<tr><td colspan="8" class="muted">No VMs registered.</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{end}}
|
|
|
|
{{define "vm_new_content"}}
|
|
<p class="muted">Create a VM and wait until the guest is fully ready. The browser will follow live create progress automatically.</p>
|
|
{{if .ErrorMessage}}
|
|
<div class="inline-error">{{.ErrorMessage}}</div>
|
|
{{end}}
|
|
<form method="post" action="/vms" class="form-grid">
|
|
{{template "csrf_field" .}}
|
|
<label>
|
|
<span>Name</span>
|
|
<input type="text" name="name" value="{{.VMCreateForm.Name}}" placeholder="generated when empty">
|
|
</label>
|
|
<label>
|
|
<span>Image</span>
|
|
<select name="image_name">
|
|
<option value="">Default image</option>
|
|
{{range .Images}}
|
|
<option value="{{.Name}}" {{if eq $.VMCreateForm.ImageName .Name}}selected{{end}}>{{.Name}}</option>
|
|
{{end}}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>vCPU</span>
|
|
<input type="number" name="vcpu" min="1" value="{{.VMCreateForm.VCPU}}">
|
|
</label>
|
|
<label>
|
|
<span>Memory (MiB)</span>
|
|
<input type="number" name="memory" min="128" value="{{.VMCreateForm.Memory}}">
|
|
</label>
|
|
<label>
|
|
<span>System Overlay Size</span>
|
|
<input type="text" name="system_overlay_size" value="{{.VMCreateForm.SystemOverlaySize}}">
|
|
</label>
|
|
<label>
|
|
<span>Work Disk Size</span>
|
|
<input type="text" name="work_disk_size" value="{{.VMCreateForm.WorkDiskSize}}">
|
|
</label>
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="nat_enabled" {{if .VMCreateForm.NATEnabled}}checked{{end}}>
|
|
<span>Enable NAT</span>
|
|
</label>
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="no_start" {{if .VMCreateForm.NoStart}}checked{{end}}>
|
|
<span>Create without starting</span>
|
|
</label>
|
|
<div class="form-actions">
|
|
<a class="button secondary" href="/vms">Cancel</a>
|
|
<button class="button" type="submit" {{if not .MutationAllowed}}disabled{{end}}>Create VM</button>
|
|
</div>
|
|
</form>
|
|
{{end}}
|
|
|
|
{{define "vm_show_content"}}
|
|
<section class="detail-grid">
|
|
<article class="detail-card">
|
|
<h2>{{.VM.Name}}</h2>
|
|
<dl>
|
|
<dt>ID</dt><dd><code>{{.VM.ID}}</code></dd>
|
|
<dt>Image</dt><dd>{{if .VMImage.ID}}<a class="table-link" href="/images/{{.VMImage.ID}}">{{.VMImage.Name}}</a>{{else}}<code>{{shortID .VM.ImageID}}</code>{{end}}</dd>
|
|
<dt>State</dt><dd><span class="state-pill {{stateClass .VM.State}}">{{.VM.State}}</span></dd>
|
|
<dt>Guest IP</dt><dd>{{if .VM.Runtime.GuestIP}}{{.VM.Runtime.GuestIP}}{{else}}-{{end}}</dd>
|
|
<dt>Created</dt><dd>{{relativeTime .VM.CreatedAt}}</dd>
|
|
</dl>
|
|
</article>
|
|
<article class="detail-card">
|
|
<h2>Configured Spec</h2>
|
|
<dl>
|
|
<dt>vCPU</dt><dd>{{.VM.Spec.VCPUCount}}</dd>
|
|
<dt>Memory</dt><dd>{{.VM.Spec.MemoryMiB}} MiB</dd>
|
|
<dt>Disk</dt><dd>{{formatBytes .VM.Spec.WorkDiskSizeBytes}}</dd>
|
|
<dt>NAT</dt><dd>{{formatBool .VM.Spec.NATEnabled}}</dd>
|
|
</dl>
|
|
</article>
|
|
<article class="detail-card">
|
|
<h2>Current Usage</h2>
|
|
<dl>
|
|
<dt>CPU</dt><dd>{{formatPercent .VMStats.CPUPercent}}</dd>
|
|
<dt>RSS</dt><dd>{{formatBytes .VMStats.RSSBytes}}</dd>
|
|
<dt>Overlay</dt><dd>{{formatBytes .VMStats.SystemOverlayBytes}}</dd>
|
|
<dt>Work Disk</dt><dd>{{formatBytes .VMStats.WorkDiskBytes}}</dd>
|
|
</dl>
|
|
</article>
|
|
</section>
|
|
|
|
<div class="section-head">
|
|
<h3>Actions</h3>
|
|
<a class="button secondary" href="/vms/{{.VM.ID}}/logs">Logs</a>
|
|
</div>
|
|
<div class="stack-inline">
|
|
{{if eq .VM.State "running"}}
|
|
<form method="post" action="/vms/{{.VM.ID}}/stop" data-confirm="Stop VM {{.VM.Name}}?">{{template "csrf_field" .}}<button class="button" type="submit" {{if not .MutationAllowed}}disabled{{end}}>Stop</button></form>
|
|
<form method="post" action="/vms/{{.VM.ID}}/restart">{{template "csrf_field" .}}<button class="button secondary" type="submit" {{if not .MutationAllowed}}disabled{{end}}>Restart</button></form>
|
|
{{else}}
|
|
<form method="post" action="/vms/{{.VM.ID}}/start">{{template "csrf_field" .}}<button class="button" type="submit" {{if not .MutationAllowed}}disabled{{end}}>Start</button></form>
|
|
{{end}}
|
|
<form method="post" action="/vms/{{.VM.ID}}/delete" data-confirm="Delete VM {{.VM.Name}}?">{{template "csrf_field" .}}<button class="button danger" type="submit" {{if not .MutationAllowed}}disabled{{end}}>Delete</button></form>
|
|
</div>
|
|
|
|
<section class="split-grid">
|
|
<div>
|
|
<div class="section-head"><h3>Listening Ports</h3></div>
|
|
{{if .VMPortsError}}
|
|
<p class="inline-error">{{.VMPortsError}}</p>
|
|
{{else}}
|
|
<table>
|
|
<thead>
|
|
<tr><th>Port</th><th>Process</th><th>Endpoint</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .VMPorts.Ports}}
|
|
<tr>
|
|
<td>{{.Proto}}/{{.Port}}</td>
|
|
<td>{{if .Process}}{{.Process}}{{else}}-{{end}}</td>
|
|
<td>{{if .Endpoint}}{{if endpointHref .Endpoint}}<a class="table-link" href="{{endpointHref .Endpoint}}" target="_blank" rel="noreferrer">{{.Endpoint}}</a>{{else}}<code>{{.Endpoint}}</code>{{end}}{{else}}-{{end}}</td>
|
|
</tr>
|
|
{{else}}
|
|
<tr><td colspan="3" class="muted">No host-reachable listeners reported.</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{end}}
|
|
</div>
|
|
<div>
|
|
<div class="section-head"><h3>Update Settings</h3></div>
|
|
<form method="post" action="/vms/{{.VM.ID}}/set" class="form-grid compact">
|
|
{{template "csrf_field" .}}
|
|
<label><span>vCPU</span><input type="number" name="vcpu" min="1" value="{{.VMSetForm.VCPU}}"></label>
|
|
<label><span>Memory (MiB)</span><input type="number" name="memory" min="128" value="{{.VMSetForm.Memory}}"></label>
|
|
<label><span>Work Disk Size</span><input type="text" name="work_disk_size" value="{{.VMSetForm.WorkDiskSize}}"></label>
|
|
<label>
|
|
<span>NAT</span>
|
|
<select name="nat_enabled">
|
|
<option value="true" {{if .VMSetForm.NATEnabled}}selected{{end}}>Enabled</option>
|
|
<option value="false" {{if not .VMSetForm.NATEnabled}}selected{{end}}>Disabled</option>
|
|
</select>
|
|
</label>
|
|
<div class="form-actions">
|
|
<a class="button secondary" href="/vms/{{.VM.ID}}">Cancel</a>
|
|
<button class="button" type="submit" {{if not .MutationAllowed}}disabled{{end}}>Save Settings</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
{{end}}
|
|
|
|
{{define "vm_logs_content"}}
|
|
<div class="section-head">
|
|
<p class="muted">Showing the last 200 lines from the Firecracker log.</p>
|
|
<div class="stack-inline">
|
|
<label class="checkbox inline"><input type="checkbox" id="log-auto-refresh"><span>Auto refresh</span></label>
|
|
<a class="button secondary" href="/vms/{{.VM.ID}}/logs">Refresh</a>
|
|
</div>
|
|
</div>
|
|
<pre class="log-output">{{.LogText}}</pre>
|
|
{{end}}
|