Compare commits

...

8 Commits
v2 ... v2.6

Author SHA1 Message Date
b4371e291e fix(build): resolve ISO version from plain v* tags (e.g. v2.6)
resolve_iso_version only matched iso/v* pattern; GUI release tags
(v2, v2.1 ... v2.6) were ignored, falling back to the old v1.0.20
annotated tag via resolve_audit_version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 23:11:33 +03:00
c22b53a406 feat(boot): set 1920x1080 resolution for framebuffer and GRUB
- Add video=1920x1080 to kernel cmdline (sets fbdev to Full HD)
- Update GRUB gfxmode to 1920x1080 (fallback to 1280x1024,auto)
- Add Xorg Monitor section with 1920x1080 Modeline and preferred mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 23:10:18 +03:00
ff0acc3698 feat(webui): server-side SVG charts + reanimator-chart viewer
Metrics:
- Replace canvas JS charts with server-side SVG via go-analyze/charts
- Add ring buffers (120 samples) for CPU temp and power
- /api/metrics/chart/{name}.svg endpoint serves live SVG, polled every 2s

Dashboard:
- Replace custom renderViewerPage with viewer.RenderHTML() from reanimator/chart submodule
- Mount chart static assets at /chart/static/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 23:07:47 +03:00
d50760e7c6 fix(webui): remove emojis from nav, fix metrics chart sizing
- Remove all emojis from sidebar nav and logo (broken on server console fonts)
- Fix canvas chart: use parentElement.getBoundingClientRect() for width,
  set explicit H=120px — fixes empty charts when offsetWidth/Height is 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 22:49:09 +03:00
ed4f8be019 fix(webui): services table — show state badge, full status on click
Replace raw systemctl output in table cell with:
- state badge (active/failed/inactive) — click to expand
- full systemctl status in collapsible pre block (max 200px scroll)
Fixes layout explosion from multi-line status text in table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 22:47:59 +03:00
883592d029 feat(desktop): switch to LightDM for X startup (matches Ubuntu LiveCD)
startx from user shell has /dev/fb0 permission issues and is fragile.
LightDM starts Xorg as root — standard LiveCD approach that works
on server hardware / IPMI KVM with nomodeset + fbdev/vesa.

- Add lightdm package, configure autologin as bee/openbox session
- Add /usr/share/xsessions/openbox.desktop
- Remove startx from .profile (LightDM manages X lifecycle)
- Remove Xwrapper.config needs_root_rights workaround (no longer needed)
- Enable lightdm.service in setup hook

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 22:17:59 +03:00
a6dcaf1c7e fix(desktop): fix X permissions for server hardware (IPMI KVM)
- Add bee user to video,input groups (fixes /dev/fb0 permission denied)
- Add Xwrapper.config: needs_root_rights=yes (X gets hw access)
- Add xserver-xorg-video-vesa as fallback driver
- Remove dead bee-tui chmod from setup hook

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 22:07:25 +03:00
88727fb590 fix(desktop): don't exec startx — fall back to shell on X failure
If X fails to start, the user gets a working shell prompt instead
of a dead session or autologin loop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 21:48:26 +03:00
17 changed files with 259 additions and 123 deletions

View File

@@ -1,3 +1,17 @@
module bee/audit
go 1.24.0
replace reanimator/chart => ../internal/chart
require (
github.com/go-analyze/charts v0.5.26
reanimator/chart v0.0.0-00010101000000-000000000000
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-analyze/bulk v0.1.3 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/image v0.24.0 // indirect
)

18
audit/go.sum Normal file
View File

@@ -0,0 +1,18 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-analyze/bulk v0.1.3 h1:pzRdBqzHDAT9PyROt0SlWE0YqPtdmTcEpIJY0C3vF0c=
github.com/go-analyze/bulk v0.1.3/go.mod h1:afon/KtFJYnekIyN20H/+XUvcLFjE8sKR1CfpqfClgM=
github.com/go-analyze/charts v0.5.26 h1:rSwZikLQuFX6cJzwI8OAgaWZneG1kDYxD857ms00ZxY=
github.com/go-analyze/charts v0.5.26/go.mod h1:s1YvQhjiSwtLx1f2dOKfiV9x2TT49nVSL6v2rlRpTbY=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -57,6 +57,7 @@ type networkManager interface {
type serviceManager interface {
ListBeeServices() ([]string, error)
ServiceState(name string) string
ServiceStatus(name string) (string, error)
ServiceDo(name string, action platform.ServiceAction) (string, error)
}
@@ -356,6 +357,10 @@ func (a *App) ListBeeServices() ([]string, error) {
return a.services.ListBeeServices()
}
func (a *App) ServiceState(name string) string {
return a.services.ServiceState(name)
}
func (a *App) ServiceStatus(name string) (string, error) {
return a.services.ServiceStatus(name)
}

View File

@@ -52,6 +52,10 @@ func (f fakeServices) ListBeeServices() ([]string, error) {
return nil, nil
}
func (f fakeServices) ServiceState(name string) string {
return "active"
}
func (f fakeServices) ServiceStatus(name string) (string, error) {
return f.serviceStatusFn(name)
}

View File

@@ -236,13 +236,15 @@ func (h *handler) handleAPIServicesList(w http.ResponseWriter, r *http.Request)
return
}
type serviceInfo struct {
Name string `json:"name"`
Status string `json:"status"`
Name string `json:"name"`
State string `json:"state"`
Body string `json:"body"`
}
result := make([]serviceInfo, 0, len(names))
for _, name := range names {
status, _ := h.opts.App.ServiceStatus(name)
result = append(result, serviceInfo{Name: name, Status: status})
state := h.opts.App.ServiceState(name)
body, _ := h.opts.App.ServiceStatus(name)
result = append(result, serviceInfo{Name: name, State: state, Body: body})
}
writeJSON(w, result)
}
@@ -421,6 +423,16 @@ func (h *handler) handleAPIMetricsStream(w http.ResponseWriter, r *http.Request)
return
case <-ticker.C:
sample := platform.SampleLiveMetrics()
// Feed ring buffers for server-side SVG charts
for _, t := range sample.Temps {
if t.Name == "CPU" {
h.ringCPUTemp.push(t.Celsius)
break
}
}
h.ringPower.push(sample.PowerW)
b, err := json.Marshal(sample)
if err != nil {
continue

View File

@@ -66,9 +66,6 @@ tr:hover td{background:#1a2030}
.form-row label{display:block;font-size:12px;color:#64748b;margin-bottom:5px}
.form-row input,.form-row select{width:100%;padding:8px 10px;background:#0f1117;border:1px solid #252d3d;border-radius:6px;color:#e2e8f0;font-size:13px;outline:none}
.form-row input:focus,.form-row select:focus{border-color:#3b82f6}
/* Metrics chart */
.chart-wrap{position:relative;height:140px;background:#0a0d14;border-radius:8px;overflow:hidden;margin-bottom:8px}
canvas.chart{width:100%;height:100%}
.chart-legend{font-size:11px;color:#64748b;padding:4px 0}
/* Grid */
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@@ -88,18 +85,18 @@ canvas.chart{width:100%;height:100%}
func layoutNav(active string) string {
items := []struct{ id, icon, label string }{
{"dashboard", "📊", "Dashboard"},
{"metrics", "📈", "Metrics"},
{"tests", "🧪", "Acceptance Tests"},
{"burn-in", "🔥", "Burn-in"},
{"network", "🌐", "Network"},
{"services", "⚙️", "Services"},
{"export", "📦", "Export"},
{"tools", "🔧", "Tools"},
{"dashboard", "", "Dashboard"},
{"metrics", "", "Metrics"},
{"tests", "", "Acceptance Tests"},
{"burn-in", "", "Burn-in"},
{"network", "", "Network"},
{"services", "", "Services"},
{"export", "", "Export"},
{"tools", "", "Tools"},
}
var b strings.Builder
b.WriteString(`<aside class="sidebar">`)
b.WriteString(`<div class="sidebar-logo">🐝 bee<span>hardware audit</span></div>`)
b.WriteString(`<div class="sidebar-logo">bee<span>hardware audit</span></div>`)
b.WriteString(`<nav class="nav">`)
for _, item := range items {
cls := "nav-item"
@@ -110,8 +107,8 @@ func layoutNav(active string) string {
if item.id != "dashboard" {
href = "/" + item.id
}
b.WriteString(fmt.Sprintf(`<a class="%s" href="%s"><span class="nav-icon">%s</span>%s</a>`,
cls, href, item.icon, item.label))
b.WriteString(fmt.Sprintf(`<a class="%s" href="%s">%s</a>`,
cls, href, item.label))
}
b.WriteString(`</nav></aside>`)
return b.String()
@@ -245,92 +242,36 @@ func renderHealthCard(opts HandlerOptions) string {
// ── Metrics ───────────────────────────────────────────────────────────────────
func renderMetrics() string {
return `<p style="color:#64748b;font-size:13px;margin-bottom:16px">Live server metrics updated every second.</p>
return `<p style="color:#64748b;font-size:13px;margin-bottom:16px">Live server metrics, charts updated every 2 seconds.</p>
<div class="grid2">
<div class="card">
<div class="card-head">GPU Metrics</div>
<div class="card-head">System</div>
<div class="card-body">
<div class="chart-wrap"><canvas id="chart-gpu-temp" class="chart"></canvas></div>
<div class="chart-legend">Temperature °C</div>
<div class="chart-wrap"><canvas id="chart-gpu-usage" class="chart"></canvas></div>
<div class="chart-legend">Usage %</div>
<div class="chart-wrap"><canvas id="chart-gpu-power" class="chart"></canvas></div>
<div class="chart-legend">Power W</div>
<div id="gpu-table"></div>
<img id="chart-cpu-temp" src="/api/metrics/chart/cpu-temp.svg" style="width:100%;border-radius:6px" alt="CPU Temp">
<img id="chart-power" src="/api/metrics/chart/power.svg" style="width:100%;border-radius:6px;margin-top:8px" alt="Power">
<div id="sys-table" style="margin-top:8px"></div>
</div>
</div>
<div class="card">
<div class="card-head">System Metrics</div>
<div class="card-head">GPU</div>
<div class="card-body">
<div class="chart-wrap"><canvas id="chart-cpu-temp" class="chart"></canvas></div>
<div class="chart-legend">CPU Temperature °C</div>
<div class="chart-wrap"><canvas id="chart-fans" class="chart"></canvas></div>
<div class="chart-legend">Fan Speed RPM</div>
<div class="chart-wrap"><canvas id="chart-power" class="chart"></canvas></div>
<div class="chart-legend">System Power W</div>
<div id="sys-table"></div>
<div id="gpu-table"><p style="color:#64748b;font-size:12px">Waiting for data...</p></div>
</div>
</div>
</div>
<script>
const WINDOW = 120;
const history = {gpuTemp:[],gpuUsage:[],gpuPower:[],cpuTemp:[],fans:[],power:[]};
const colors = ['#60a5fa','#34d399','#f87171','#fbbf24','#a78bfa','#fb7185'];
function push(arr, val) { arr.push(val); if (arr.length > WINDOW) arr.shift(); }
function drawChart(canvasId, datasets, maxY) {
const c = document.getElementById(canvasId);
if (!c) return;
const W = c.offsetWidth, H = c.offsetHeight;
c.width = W; c.height = H;
const ctx = c.getContext('2d');
ctx.clearRect(0,0,W,H);
ctx.fillStyle = '#0a0d14';
ctx.fillRect(0,0,W,H);
// grid
ctx.strokeStyle = '#1e2535';
ctx.lineWidth = 1;
for (let y = 0; y <= 4; y++) {
const py = H * y / 4;
ctx.beginPath(); ctx.moveTo(0,py); ctx.lineTo(W,py); ctx.stroke();
}
const max = maxY || datasets.reduce((m,d) => Math.max(m,...d.map(v=>v||0)), 1) || 1;
datasets.forEach((data, i) => {
if (!data.length) return;
ctx.strokeStyle = colors[i % colors.length];
ctx.lineWidth = 1.5;
ctx.beginPath();
data.forEach((v, j) => {
const x = W * j / Math.max(data.length - 1, 1);
const y = H - H * (v || 0) / max;
j === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
});
ctx.stroke();
function refreshCharts() {
const t = '?t=' + Date.now();
['chart-cpu-temp','chart-power'].forEach(id => {
const el = document.getElementById(id);
if (el) el.src = el.src.split('?')[0] + t;
});
}
setInterval(refreshCharts, 2000);
const es = new EventSource('/api/metrics/stream');
es.addEventListener('metrics', e => {
const d = JSON.parse(e.data);
// GPU
const gpuTemps = (d.gpus||[]).map(g => g.temp_c||0);
const gpuUsages = (d.gpus||[]).map(g => g.usage_pct||0);
const gpuPowers = (d.gpus||[]).map(g => g.power_w||0);
if (!history.gpuTemp.length && gpuTemps.length) {
history.gpuTemp = gpuTemps.map(() => []);
history.gpuUsage = gpuUsages.map(() => []);
history.gpuPower = gpuPowers.map(() => []);
}
gpuTemps.forEach((v,i) => push(history.gpuTemp[i]||[], v));
gpuUsages.forEach((v,i) => push(history.gpuUsage[i]||[], v));
gpuPowers.forEach((v,i) => push(history.gpuPower[i]||[], v));
drawChart('chart-gpu-temp', history.gpuTemp.length ? history.gpuTemp : [history.cpuTemp]);
drawChart('chart-gpu-usage', history.gpuUsage, 100);
drawChart('chart-gpu-power', history.gpuPower);
// GPU table
const gpuRows = (d.gpus||[]).map(g =>
'<tr><td>GPU '+g.index+'</td><td>'+g.temp_c+'°C</td><td>'+g.usage_pct+'%</td><td>'+g.power_w+'W</td><td>'+g.clock_mhz+'MHz</td></tr>'
).join('');
@@ -338,27 +279,16 @@ es.addEventListener('metrics', e => {
'<table><tr><th>GPU</th><th>Temp</th><th>Usage</th><th>Power</th><th>Clock</th></tr>'+gpuRows+'</table>' :
'<p style="color:#64748b;font-size:12px">No NVIDIA GPU detected</p>';
// System
const cpuTemp = (d.temps||[]).find(t => t.name==='CPU');
push(history.cpuTemp, cpuTemp ? cpuTemp.celsius : 0);
const fanRPMs = (d.fans||[]).map(f => f.rpm||0);
if (!history.fans.length && fanRPMs.length) history.fans = fanRPMs.map(() => []);
fanRPMs.forEach((v,i) => push(history.fans[i]||[], v));
push(history.power, d.power_w||0);
drawChart('chart-cpu-temp', [history.cpuTemp]);
drawChart('chart-fans', history.fans.length ? history.fans : [[]]);
drawChart('chart-power', [history.power]);
// Sys table
let sysHTML = '';
const cpuTemp = (d.temps||[]).find(t => t.name==='CPU');
if (cpuTemp) sysHTML += '<tr><td>CPU Temp</td><td>'+cpuTemp.celsius.toFixed(1)+'°C</td></tr>';
(d.fans||[]).forEach(f => sysHTML += '<tr><td>'+f.name+'</td><td>'+f.rpm+' RPM</td></tr>');
if (d.power_w) sysHTML += '<tr><td>System Power</td><td>'+d.power_w.toFixed(0)+'W</td></tr>';
document.getElementById('sys-table').innerHTML = sysHTML ?
'<table style="margin-top:8px">'+sysHTML+'</table>' :
'<p style="color:#64748b;font-size:12px">No sensor data (requires ipmitool/sensors)</p>';
'<table>'+sysHTML+'</table>' :
'<p style="color:#64748b;font-size:12px">No sensor data (ipmitool/sensors required)</p>';
});
es.onerror = () => { /* reconnect automatically */ };
es.onerror = () => {};
</script>`
}
@@ -515,9 +445,16 @@ func renderServices() string {
function loadServices() {
fetch('/api/services').then(r=>r.json()).then(svcs => {
const rows = svcs.map(s => {
const st = s.status||'unknown';
const badge = st.includes('active') ? 'badge-ok' : st.includes('failed') ? 'badge-err' : 'badge-warn';
return '<tr><td>'+s.name+'</td><td><span class="badge '+badge+'">'+st+'</span></td><td>' +
const st = s.state||'unknown';
const badge = st==='active' ? 'badge-ok' : st==='failed' ? 'badge-err' : 'badge-warn';
const id = 'svc-body-'+s.name.replace(/[^a-z0-9]/g,'-');
const body = (s.body||'').replace(/</g,'&lt;').replace(/>/g,'&gt;');
return '<tr>' +
'<td style="white-space:nowrap">'+s.name+'</td>' +
'<td style="white-space:nowrap"><span class="badge '+badge+'" style="cursor:pointer" onclick="toggleBody(\''+id+'\')">'+st+' ▾</span>' +
'<div id="'+id+'" style="display:none;margin-top:6px"><pre style="font-size:11px;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;background:#0a0d14;padding:8px;border-radius:6px;color:#94a3b8">'+body+'</pre></div>' +
'</td>' +
'<td style="white-space:nowrap">' +
'<button class="btn btn-sm btn-secondary" onclick="svcAction(\''+s.name+'\',\'start\')">Start</button> ' +
'<button class="btn btn-sm btn-secondary" onclick="svcAction(\''+s.name+'\',\'stop\')">Stop</button> ' +
'<button class="btn btn-sm btn-secondary" onclick="svcAction(\''+s.name+'\',\'restart\')">Restart</button>' +
@@ -527,6 +464,10 @@ function loadServices() {
'<table><tr><th>Service</th><th>Status</th><th>Actions</th></tr>'+rows+'</table>';
});
}
function toggleBody(id) {
const el = document.getElementById(id);
if (el) el.style.display = el.style.display==='none' ? 'block' : 'none';
}
function svcAction(name, action) {
fetch('/api/services/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,action})})
.then(r=>r.json()).then(d => {

View File

@@ -8,9 +8,14 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
"bee/audit/internal/app"
"bee/audit/internal/runtimeenv"
gocharts "github.com/go-analyze/charts"
"reanimator/chart/viewer"
"reanimator/chart/web"
)
const defaultTitle = "Bee Hardware Audit"
@@ -24,10 +29,49 @@ type HandlerOptions struct {
RuntimeMode runtimeenv.Mode
}
// metricsRing holds a rolling window of live metric samples.
type metricsRing struct {
mu sync.Mutex
vals []float64
labels []string
size int
}
func newMetricsRing(size int) *metricsRing {
return &metricsRing{size: size, vals: make([]float64, 0, size), labels: make([]string, 0, size)}
}
func (r *metricsRing) push(v float64) {
r.mu.Lock()
defer r.mu.Unlock()
if len(r.vals) >= r.size {
r.vals = r.vals[1:]
r.labels = r.labels[1:]
}
r.vals = append(r.vals, v)
r.labels = append(r.labels, time.Now().Format("15:04"))
}
func (r *metricsRing) snapshot() ([]float64, []string) {
r.mu.Lock()
defer r.mu.Unlock()
v := make([]float64, len(r.vals))
l := make([]string, len(r.labels))
copy(v, r.vals)
copy(l, r.labels)
return v, l
}
// handler is the HTTP handler for the web UI.
type handler struct {
opts HandlerOptions
mux *http.ServeMux
opts HandlerOptions
mux *http.ServeMux
ringCPUTemp *metricsRing
ringPower *metricsRing
ringFans []*metricsRing
ringGPUTemp []*metricsRing
ringGPUUtil []*metricsRing
ringsMu sync.Mutex
}
// NewHandler creates the HTTP mux with all routes.
@@ -42,7 +86,11 @@ func NewHandler(opts HandlerOptions) http.Handler {
opts.RuntimeMode = runtimeenv.ModeAuto
}
h := &handler{opts: opts}
h := &handler{
opts: opts,
ringCPUTemp: newMetricsRing(120),
ringPower: newMetricsRing(120),
}
mux := http.NewServeMux()
// ── Infrastructure ──────────────────────────────────────────────────────
@@ -87,8 +135,12 @@ func NewHandler(opts HandlerOptions) http.Handler {
// Preflight
mux.HandleFunc("GET /api/preflight", h.handleAPIPreflight)
// Metrics — SSE stream of live sensor data
// Metrics — SSE stream of live sensor data + server-side SVG charts
mux.HandleFunc("GET /api/metrics/stream", h.handleAPIMetricsStream)
mux.HandleFunc("GET /api/metrics/chart/", h.handleMetricsChartSVG)
// Reanimator chart static assets
mux.Handle("GET /chart/static/", http.StripPrefix("/chart/static/", web.Static()))
// ── Pages ────────────────────────────────────────────────────────────────
mux.HandleFunc("GET /", h.handlePage)
@@ -181,10 +233,72 @@ func (h *handler) handleExportIndex(w http.ResponseWriter, r *http.Request) {
func (h *handler) handleViewer(w http.ResponseWriter, r *http.Request) {
snapshot, _ := loadSnapshot(h.opts.AuditPath)
body := renderViewerPage(h.opts.Title, snapshot)
body, err := viewer.RenderHTML(snapshot, h.opts.Title)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(body))
_, _ = w.Write(body)
}
func (h *handler) handleMetricsChartSVG(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/api/metrics/chart/")
name = strings.TrimSuffix(name, ".svg")
var ring *metricsRing
var title, unit string
switch name {
case "cpu-temp":
ring, title, unit = h.ringCPUTemp, "CPU Temperature", "°C"
case "power":
ring, title, unit = h.ringPower, "System Power", "W"
default:
http.NotFound(w, r)
return
}
vals, labels := ring.snapshot()
if len(vals) == 0 {
vals = []float64{0}
labels = []string{""}
}
// Sparse x-axis labels
sparse := make([]string, len(labels))
step := len(labels) / 6
if step < 1 {
step = 1
}
for i := range labels {
if i%step == 0 {
sparse[i] = labels[i]
}
}
opt := gocharts.NewLineChartOptionWithData([][]float64{vals})
opt.Title = gocharts.TitleOption{Text: title + " (" + unit + ")"}
opt.XAxis.Labels = sparse
opt.Legend = gocharts.LegendOption{Show: gocharts.Ptr(false)}
p := gocharts.NewPainter(gocharts.PainterOptions{
OutputFormat: gocharts.ChartOutputSVG,
Width: 600,
Height: 180,
}, gocharts.PainterThemeOption(gocharts.GetTheme("grafana")))
if err := p.LineChart(opt); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buf, err := p.Bytes()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(buf)
}
// ── Page handler ─────────────────────────────────────────────────────────────

View File

@@ -32,6 +32,6 @@ lb config noauto \
--memtest none \
--iso-volume "EASY-BEE" \
--iso-application "EASY-BEE" \
--bootappend-live "boot=live components quiet nomodeset console=tty0 console=ttyS0,115200n8 loglevel=3 username=bee user-fullname=Bee modprobe.blacklist=nouveau" \
--bootappend-live "boot=live components quiet nomodeset video=1920x1080 console=tty0 console=ttyS0,115200n8 loglevel=3 username=bee user-fullname=Bee modprobe.blacklist=nouveau" \
--apt-recommends false \
"${@}"

View File

@@ -84,6 +84,15 @@ resolve_iso_version() {
;;
esac
# Also accept plain v* tags (e.g. v2, v2.1 used for GUI releases)
tag="$(git -C "${REPO_ROOT}" describe --tags --match 'v*' --abbrev=7 --dirty 2>/dev/null || true)"
case "${tag}" in
v*)
echo "${tag#v}"
return 0
;;
esac
# Fall back to audit version so the name is still meaningful
resolve_audit_version
}

View File

@@ -8,7 +8,7 @@ else
fi
if loadfont $font ; then
set gfxmode=800x600
set gfxmode=1920x1080,1280x1024,auto
set gfxpayload=keep
insmod efi_gop
insmod efi_uga

View File

@@ -15,7 +15,7 @@ ensure_bee_console_user() {
mkdir -p /home/bee
chown -R bee:bee /home/bee
echo "bee:eeb" | chpasswd
usermod -aG sudo bee 2>/dev/null || true
usermod -aG sudo,video,input bee 2>/dev/null || true
}
ensure_bee_console_user
@@ -29,6 +29,7 @@ systemctl enable bee-audit.service
systemctl enable bee-web.service
systemctl enable bee-sshsetup.service
systemctl enable ssh.service
systemctl enable lightdm.service 2>/dev/null || true
systemctl enable qemu-guest-agent.service 2>/dev/null || true
systemctl enable serial-getty@ttyS0.service 2>/dev/null || true
systemctl enable serial-getty@ttyS1.service 2>/dev/null || true
@@ -39,7 +40,6 @@ chmod +x /usr/local/bin/bee-network.sh 2>/dev/null || true
chmod +x /usr/local/bin/bee-nvidia-load 2>/dev/null || true
chmod +x /usr/local/bin/bee-sshsetup 2>/dev/null || true
chmod +x /usr/local/bin/bee-smoketest 2>/dev/null || true
chmod +x /usr/local/bin/bee-tui 2>/dev/null || true
chmod +x /usr/local/bin/bee 2>/dev/null || true
chmod +x /usr/local/bin/bee-log-run 2>/dev/null || true

View File

@@ -52,10 +52,11 @@ qrencode
openbox
tint2
xorg
xinit
xterm
chromium
xserver-xorg-video-fbdev
xserver-xorg-video-vesa
lightdm
# Firmware
firmware-linux-free

View File

@@ -0,0 +1,2 @@
allowed_users=anybody
needs_root_rights=yes

View File

@@ -4,7 +4,19 @@ Section "Device"
Option "fbdev" "/dev/fb0"
EndSection
Section "Monitor"
Identifier "monitor0"
Modeline "1920x1080" 148.50 1920 2008 2052 2200 1080 1084 1089 1125 +hsync +vsync
Option "PreferredMode" "1920x1080"
EndSection
Section "Screen"
Identifier "screen0"
Device "fbdev"
Monitor "monitor0"
DefaultDepth 24
SubSection "Display"
Depth 24
Modes "1920x1080" "1280x1024" "1024x768"
EndSubSection
EndSection

View File

@@ -0,0 +1,5 @@
[Seat:*]
autologin-user=bee
autologin-user-timeout=0
autologin-session=openbox
user-session=openbox

View File

@@ -1,8 +1 @@
export PATH="/usr/local/bin:$PATH"
# On tty1 (IPMI KVM console): start X desktop directly on VT1
# so the KVM shows the graphical UI without switching VTs.
if [ "$(tty 2>/dev/null)" = "/dev/tty1" ] \
&& [ -z "${DISPLAY:-}" ]; then
exec startx /usr/local/bin/bee-openbox-session -- :0 vt1 -nolisten tcp
fi

View File

@@ -0,0 +1,6 @@
[Desktop Entry]
Name=Openbox
Comment=Bee Hardware Audit Desktop
Exec=/usr/local/bin/bee-openbox-session
TryExec=openbox
Type=Application