UI: amber accents, smaller wallpaper logo, new support bundle name, drop display resolution
- Bootloader: GRUB fallback text colors → yellow/brown (amber tone) - CLI charts: all GPU metric series use single amber color (xterm-256 #214) - Wallpaper: logo width scaled to 400 px dynamically, shadow scales with font size - Support bundle: renamed to YYYY-MM-DD (BEE-SP vX.X) SRV_MODEL SRV_SN ToD.tar.gz using dmidecode for server model (spaces→underscores) and serial number - Remove display resolution feature (UI card, API routes, handlers, tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -192,7 +192,7 @@ var supportBundleOptionalFiles = []struct {
|
|||||||
{name: "system/syslog.txt", src: "/var/log/syslog"},
|
{name: "system/syslog.txt", src: "/var/log/syslog"},
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportBundleGlob = "bee-support-*.tar.gz"
|
const supportBundleGlob = "????-??-?? (BEE-SP*)*.tar.gz"
|
||||||
|
|
||||||
func BuildSupportBundle(exportDir string) (string, error) {
|
func BuildSupportBundle(exportDir string) (string, error) {
|
||||||
exportDir = strings.TrimSpace(exportDir)
|
exportDir = strings.TrimSpace(exportDir)
|
||||||
@@ -206,9 +206,14 @@ func BuildSupportBundle(exportDir string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
host := sanitizeFilename(hostnameOr("unknown"))
|
now := time.Now().UTC()
|
||||||
ts := time.Now().UTC().Format("20060102-150405")
|
date := now.Format("2006-01-02")
|
||||||
stageRoot := filepath.Join(os.TempDir(), fmt.Sprintf("bee-support-%s-%s", host, ts))
|
tod := now.Format("15:04:05")
|
||||||
|
ver := bundleVersion()
|
||||||
|
model := serverModelForBundle()
|
||||||
|
sn := serverSerialForBundle()
|
||||||
|
|
||||||
|
stageRoot := filepath.Join(os.TempDir(), fmt.Sprintf("bee-support-stage-%s-%s", sanitizeFilename(hostnameOr("unknown")), now.Format("20060102-150405")))
|
||||||
if err := os.MkdirAll(stageRoot, 0755); err != nil {
|
if err := os.MkdirAll(stageRoot, 0755); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -240,7 +245,8 @@ func BuildSupportBundle(exportDir string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
archivePath := filepath.Join(os.TempDir(), fmt.Sprintf("bee-support-%s-%s.tar.gz", host, ts))
|
archiveName := fmt.Sprintf("%s (BEE-SP v%s) %s %s %s.tar.gz", date, ver, model, sn, tod)
|
||||||
|
archivePath := filepath.Join(os.TempDir(), archiveName)
|
||||||
if err := createSupportTarGz(archivePath, stageRoot); err != nil {
|
if err := createSupportTarGz(archivePath, stageRoot); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -397,6 +403,60 @@ func writeManifest(dst, exportDir, stageRoot string) error {
|
|||||||
return os.WriteFile(dst, []byte(body.String()), 0644)
|
return os.WriteFile(dst, []byte(body.String()), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bundleVersion() string {
|
||||||
|
v := buildVersion()
|
||||||
|
v = strings.TrimPrefix(v, "v")
|
||||||
|
v = strings.TrimPrefix(v, "V")
|
||||||
|
if v == "" || v == "unknown" {
|
||||||
|
return "0.0"
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverModelForBundle() string {
|
||||||
|
raw, err := exec.Command("dmidecode", "-t", "1").Output()
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(raw), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
key, val, ok := strings.Cut(line, ": ")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(key) == "Product Name" {
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
if val == "" {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(val, " ", "_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverSerialForBundle() string {
|
||||||
|
raw, err := exec.Command("dmidecode", "-t", "1").Output()
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(raw), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
key, val, ok := strings.Cut(line, ": ")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(key) == "Serial Number" {
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
if val == "" {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
func buildVersion() string {
|
func buildVersion() string {
|
||||||
raw, err := exec.Command("bee", "version").CombinedOutput()
|
raw, err := exec.Command("bee", "version").CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -383,10 +383,7 @@ func drawGPUChartSVG(rows []GPUMetricRow, gpuIdx int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ansiRed = "\033[31m"
|
ansiAmber = "\033[38;5;214m"
|
||||||
ansiBlue = "\033[34m"
|
|
||||||
ansiGreen = "\033[32m"
|
|
||||||
ansiYellow = "\033[33m"
|
|
||||||
ansiReset = "\033[0m"
|
ansiReset = "\033[0m"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -415,10 +412,10 @@ func RenderGPUTerminalChart(rows []GPUMetricRow) string {
|
|||||||
fn func(GPUMetricRow) float64
|
fn func(GPUMetricRow) float64
|
||||||
}
|
}
|
||||||
defs := []seriesDef{
|
defs := []seriesDef{
|
||||||
{"Temperature (°C)", ansiRed, func(r GPUMetricRow) float64 { return r.TempC }},
|
{"Temperature (°C)", ansiAmber, func(r GPUMetricRow) float64 { return r.TempC }},
|
||||||
{"GPU Usage (%)", ansiBlue, func(r GPUMetricRow) float64 { return r.UsagePct }},
|
{"GPU Usage (%)", ansiAmber, func(r GPUMetricRow) float64 { return r.UsagePct }},
|
||||||
{"Power (W)", ansiGreen, func(r GPUMetricRow) float64 { return r.PowerW }},
|
{"Power (W)", ansiAmber, func(r GPUMetricRow) float64 { return r.PowerW }},
|
||||||
{"Clock (MHz)", ansiYellow, func(r GPUMetricRow) float64 { return r.ClockMHz }},
|
{"Clock (MHz)", ansiAmber, func(r GPUMetricRow) float64 { return r.ClockMHz }},
|
||||||
}
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|||||||
@@ -1376,107 +1376,3 @@ func (h *handler) rollbackPendingNetworkChange() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Display / Screen Resolution ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
type displayMode struct {
|
|
||||||
Output string `json:"output"`
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
Current bool `json:"current"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type displayInfo struct {
|
|
||||||
Output string `json:"output"`
|
|
||||||
Modes []displayMode `json:"modes"`
|
|
||||||
Current string `json:"current"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var xrandrOutputRE = regexp.MustCompile(`^(\S+)\s+connected`)
|
|
||||||
var xrandrModeRE = regexp.MustCompile(`^\s{3}(\d+x\d+)\s`)
|
|
||||||
var xrandrCurrentRE = regexp.MustCompile(`\*`)
|
|
||||||
|
|
||||||
func parseXrandrOutput(out string) []displayInfo {
|
|
||||||
var infos []displayInfo
|
|
||||||
var cur *displayInfo
|
|
||||||
for _, line := range strings.Split(out, "\n") {
|
|
||||||
if m := xrandrOutputRE.FindStringSubmatch(line); m != nil {
|
|
||||||
if cur != nil {
|
|
||||||
infos = append(infos, *cur)
|
|
||||||
}
|
|
||||||
cur = &displayInfo{Output: m[1]}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if cur == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if m := xrandrModeRE.FindStringSubmatch(line); m != nil {
|
|
||||||
isCurrent := xrandrCurrentRE.MatchString(line)
|
|
||||||
mode := displayMode{Output: cur.Output, Mode: m[1], Current: isCurrent}
|
|
||||||
cur.Modes = append(cur.Modes, mode)
|
|
||||||
if isCurrent {
|
|
||||||
cur.Current = m[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cur != nil {
|
|
||||||
infos = append(infos, *cur)
|
|
||||||
}
|
|
||||||
return infos
|
|
||||||
}
|
|
||||||
|
|
||||||
func xrandrCommand(args ...string) *exec.Cmd {
|
|
||||||
cmd := exec.Command("xrandr", args...)
|
|
||||||
env := append([]string{}, os.Environ()...)
|
|
||||||
hasDisplay := false
|
|
||||||
hasXAuthority := false
|
|
||||||
for _, kv := range env {
|
|
||||||
if strings.HasPrefix(kv, "DISPLAY=") && strings.TrimPrefix(kv, "DISPLAY=") != "" {
|
|
||||||
hasDisplay = true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(kv, "XAUTHORITY=") && strings.TrimPrefix(kv, "XAUTHORITY=") != "" {
|
|
||||||
hasXAuthority = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasDisplay {
|
|
||||||
env = append(env, "DISPLAY=:0")
|
|
||||||
}
|
|
||||||
if !hasXAuthority {
|
|
||||||
env = append(env, "XAUTHORITY=/home/bee/.Xauthority")
|
|
||||||
}
|
|
||||||
cmd.Env = env
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) handleAPIDisplayResolutions(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
out, err := xrandrCommand().Output()
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, http.StatusInternalServerError, "xrandr: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeJSON(w, parseXrandrOutput(string(out)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) handleAPIDisplaySet(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req struct {
|
|
||||||
Output string `json:"output"`
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Output == "" || req.Mode == "" {
|
|
||||||
writeError(w, http.StatusBadRequest, "output and mode are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Validate mode looks like WxH to prevent injection
|
|
||||||
if !regexp.MustCompile(`^\d+x\d+$`).MatchString(req.Mode) {
|
|
||||||
writeError(w, http.StatusBadRequest, "invalid mode format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Validate output name (no special chars)
|
|
||||||
if !regexp.MustCompile(`^[A-Za-z0-9_\-]+$`).MatchString(req.Output) {
|
|
||||||
writeError(w, http.StatusBadRequest, "invalid output name")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if out, err := xrandrCommand("--output", req.Output, "--mode", req.Mode).CombinedOutput(); err != nil {
|
|
||||||
writeError(w, http.StatusInternalServerError, "xrandr: "+strings.TrimSpace(string(out)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeJSON(w, map[string]string{"status": "ok", "output": req.Output, "mode": req.Mode})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,30 +10,6 @@ import (
|
|||||||
"bee/audit/internal/platform"
|
"bee/audit/internal/platform"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestXrandrCommandAddsDefaultX11Env(t *testing.T) {
|
|
||||||
t.Setenv("DISPLAY", "")
|
|
||||||
t.Setenv("XAUTHORITY", "")
|
|
||||||
|
|
||||||
cmd := xrandrCommand("--query")
|
|
||||||
|
|
||||||
var hasDisplay bool
|
|
||||||
var hasXAuthority bool
|
|
||||||
for _, kv := range cmd.Env {
|
|
||||||
if kv == "DISPLAY=:0" {
|
|
||||||
hasDisplay = true
|
|
||||||
}
|
|
||||||
if kv == "XAUTHORITY=/home/bee/.Xauthority" {
|
|
||||||
hasXAuthority = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasDisplay {
|
|
||||||
t.Fatalf("DISPLAY not injected: %v", cmd.Env)
|
|
||||||
}
|
|
||||||
if !hasXAuthority {
|
|
||||||
t.Fatalf("XAUTHORITY not injected: %v", cmd.Env)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleAPISATRunDecodesBodyWithoutContentLength(t *testing.T) {
|
func TestHandleAPISATRunDecodesBodyWithoutContentLength(t *testing.T) {
|
||||||
globalQueue.mu.Lock()
|
globalQueue.mu.Lock()
|
||||||
originalTasks := globalQueue.tasks
|
originalTasks := globalQueue.tasks
|
||||||
|
|||||||
@@ -2849,55 +2849,6 @@ usbRefresh();
|
|||||||
</script>`
|
</script>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Display Resolution ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func renderDisplayInline() string {
|
|
||||||
return `<div id="display-status" style="color:var(--muted);font-size:13px;margin-bottom:12px">Loading displays...</div>
|
|
||||||
<div id="display-controls"></div>
|
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
function loadDisplays() {
|
|
||||||
fetch('/api/display/resolutions').then(r=>r.json()).then(displays => {
|
|
||||||
const status = document.getElementById('display-status');
|
|
||||||
const ctrl = document.getElementById('display-controls');
|
|
||||||
if (!displays || displays.length === 0) {
|
|
||||||
status.textContent = 'No connected displays found or xrandr not available.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
status.textContent = '';
|
|
||||||
ctrl.innerHTML = displays.map(d => {
|
|
||||||
const opts = (d.modes||[]).map(m =>
|
|
||||||
'<option value="'+m.mode+'"'+(m.current?' selected':'')+'>'+m.mode+(m.current?' (current)':'')+'</option>'
|
|
||||||
).join('');
|
|
||||||
return '<div style="margin-bottom:12px">'
|
|
||||||
+'<span style="font-weight:600;margin-right:8px">'+d.output+'</span>'
|
|
||||||
+'<span style="color:var(--muted);font-size:12px;margin-right:12px">Current: '+d.current+'</span>'
|
|
||||||
+'<select id="res-sel-'+d.output+'" style="margin-right:8px">'+opts+'</select>'
|
|
||||||
+'<button class="btn btn-sm btn-primary" onclick="applyResolution(\''+d.output+'\')">Apply</button>'
|
|
||||||
+'</div>';
|
|
||||||
}).join('');
|
|
||||||
}).catch(()=>{
|
|
||||||
document.getElementById('display-status').textContent = 'xrandr not available on this system.';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
window.applyResolution = function(output) {
|
|
||||||
const sel = document.getElementById('res-sel-'+output);
|
|
||||||
if (!sel) return;
|
|
||||||
const mode = sel.value;
|
|
||||||
const btn = sel.nextElementSibling;
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Applying...';
|
|
||||||
fetch('/api/display/set', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({output:output,mode:mode})})
|
|
||||||
.then(r=>r.json()).then(d=>{
|
|
||||||
if (d.error) { alert('Error: '+d.error); }
|
|
||||||
loadDisplays();
|
|
||||||
}).catch(e=>{ alert('Error: '+e); })
|
|
||||||
.finally(()=>{ btn.disabled=false; btn.textContent='Apply'; });
|
|
||||||
};
|
|
||||||
loadDisplays();
|
|
||||||
})();
|
|
||||||
</script>`
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderNvidiaSelfHealInline() string {
|
func renderNvidiaSelfHealInline() string {
|
||||||
return `<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Inspect NVIDIA GPU health, restart the bee-nvidia driver service, and issue a per-GPU reset when the driver reports reset required.</p>
|
return `<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Inspect NVIDIA GPU health, restart the bee-nvidia driver service, and issue a per-GPU reset when the driver reports reset required.</p>
|
||||||
@@ -3086,8 +3037,6 @@ function installToRAM() {
|
|||||||
<div class="card"><div class="card-head">Services</div><div class="card-body">` +
|
<div class="card"><div class="card-head">Services</div><div class="card-body">` +
|
||||||
renderServicesInline() + `</div></div>
|
renderServicesInline() + `</div></div>
|
||||||
|
|
||||||
<div class="card"><div class="card-head">Display Resolution</div><div class="card-body">` +
|
|
||||||
renderDisplayInline() + `</div></div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function checkTools() {
|
function checkTools() {
|
||||||
|
|||||||
@@ -295,10 +295,6 @@ func NewHandler(opts HandlerOptions) http.Handler {
|
|||||||
// Tools
|
// Tools
|
||||||
mux.HandleFunc("GET /api/tools/check", h.handleAPIToolsCheck)
|
mux.HandleFunc("GET /api/tools/check", h.handleAPIToolsCheck)
|
||||||
|
|
||||||
// Display
|
|
||||||
mux.HandleFunc("GET /api/display/resolutions", h.handleAPIDisplayResolutions)
|
|
||||||
mux.HandleFunc("POST /api/display/set", h.handleAPIDisplaySet)
|
|
||||||
|
|
||||||
// GPU presence / tools
|
// GPU presence / tools
|
||||||
mux.HandleFunc("GET /api/gpu/presence", h.handleAPIGPUPresence)
|
mux.HandleFunc("GET /api/gpu/presence", h.handleAPIGPUPresence)
|
||||||
mux.HandleFunc("GET /api/gpu/nvidia", h.handleAPIGNVIDIAGPUs)
|
mux.HandleFunc("GET /api/gpu/nvidia", h.handleAPIGNVIDIAGPUs)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
set color_normal=light-gray/black
|
set color_normal=light-gray/black
|
||||||
set color_highlight=white/dark-gray
|
set color_highlight=yellow/black
|
||||||
|
|
||||||
if [ -e /boot/grub/splash.png ]; then
|
if [ -e /boot/grub/splash.png ]; then
|
||||||
set theme=/boot/grub/live-theme/theme.txt
|
set theme=/boot/grub/live-theme/theme.txt
|
||||||
else
|
else
|
||||||
set menu_color_normal=cyan/black
|
set menu_color_normal=yellow/black
|
||||||
set menu_color_highlight=white/dark-gray
|
set menu_color_highlight=white/brown
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -82,16 +82,22 @@ glow_draw.ellipse((520, 340, 1400, 760), fill=(255, 190, 40, 36))
|
|||||||
glow = glow.filter(ImageFilter.GaussianBlur(60))
|
glow = glow.filter(ImageFilter.GaussianBlur(60))
|
||||||
img = Image.alpha_composite(img.convert('RGBA'), glow)
|
img = Image.alpha_composite(img.convert('RGBA'), glow)
|
||||||
|
|
||||||
font_logo = load_font(MONO_FONT_CANDIDATES, 64)
|
TARGET_LOGO_W = 400
|
||||||
|
max_chars = max(len(line) for line in ASCII_ART)
|
||||||
|
_probe_font = load_font(MONO_FONT_CANDIDATES, 64)
|
||||||
|
_probe_cw, _ = mono_metrics(_probe_font)
|
||||||
|
font_size_logo = max(6, int(64 * TARGET_LOGO_W / (_probe_cw * max_chars)))
|
||||||
|
font_logo = load_font(MONO_FONT_CANDIDATES, font_size_logo)
|
||||||
char_w, char_h = mono_metrics(font_logo)
|
char_w, char_h = mono_metrics(font_logo)
|
||||||
logo_mask = render_ascii_mask(font_logo, ASCII_ART, char_w, char_h, 8)
|
logo_mask = render_ascii_mask(font_logo, ASCII_ART, char_w, char_h, 2)
|
||||||
logo_w, logo_h = logo_mask.size
|
logo_w, logo_h = logo_mask.size
|
||||||
logo_x = (W - logo_w) // 2
|
logo_x = (W - logo_w) // 2
|
||||||
logo_y = 270
|
logo_y = 380
|
||||||
|
|
||||||
shadow_mask = logo_mask.filter(ImageFilter.GaussianBlur(2))
|
sh_off = max(1, font_size_logo // 6)
|
||||||
img.paste(SHADOW, (logo_x + 16, logo_y + 14), shadow_mask)
|
shadow_mask = logo_mask.filter(ImageFilter.GaussianBlur(1))
|
||||||
img.paste(FG_DIM, (logo_x + 8, logo_y + 7), logo_mask)
|
img.paste(SHADOW, (logo_x + sh_off * 2, logo_y + sh_off * 2), shadow_mask)
|
||||||
|
img.paste(FG_DIM, (logo_x + sh_off, logo_y + sh_off), logo_mask)
|
||||||
img.paste(FG, (logo_x, logo_y), logo_mask)
|
img.paste(FG, (logo_x, logo_y), logo_mask)
|
||||||
|
|
||||||
font_sub = load_font(SUB_FONT_CANDIDATES, 30)
|
font_sub = load_font(SUB_FONT_CANDIDATES, 30)
|
||||||
|
|||||||
Reference in New Issue
Block a user