detached d4c31ca

feat(webui): show detected hardware in Performance tab

commit
d4c31caf23c5ffc97d66dd51e1c0890d49dbf26b
author
brice · · 2025-12-28 17:36 UTC
committer
brice · · 2025-12-29 17:28 UTC
parent
80b95cf
signature
unsigned
2 files · +81 -14
M internal/infrastructure/webkit/scheme_handler.go +54 -13
@@ -13,7 +13,9 @@ import (
1313 "strings"
1414 "sync"
1515
16+ "github.com/bnema/dumber/internal/application/port"
1617 "github.com/bnema/dumber/internal/infrastructure/config"
18+ "github.com/bnema/dumber/internal/infrastructure/env"
1719 "github.com/bnema/dumber/internal/logging"
1820 "github.com/bnema/puregotk-webkit/soup"
1921 "github.com/bnema/puregotk-webkit/webkit"
@@ -59,11 +61,13 @@ func (f PageHandlerFunc) Handle(req *SchemeRequest) *SchemeResponse {
5961
6062 // DumbSchemeHandler handles dumb:// URI scheme requests.
6163 type DumbSchemeHandler struct {
62- handlers map[string]PageHandler
63- assets embed.FS
64- assetDir string // subdirectory within embed.FS (e.g., "assets/webui")
65- logger zerolog.Logger
66- mu sync.RWMutex
64+ handlers map[string]PageHandler
65+ assets embed.FS
66+ assetDir string // subdirectory within embed.FS (e.g., "assets/webui")
67+ logger zerolog.Logger
68+ mu sync.RWMutex
69+ hwSurveyor *env.HardwareSurveyor
70+ ctx context.Context
6771 }
6872
6973 type configAppearancePayload struct {
@@ -76,9 +80,19 @@ type configAppearancePayload struct {
7680 DarkPalette config.ColorPalette `json:"dark_palette"`
7781 }
7882
83+type configHardwarePayload struct {
84+ CPUCores int `json:"cpu_cores"`
85+ CPUThreads int `json:"cpu_threads"`
86+ TotalRAMMB int `json:"total_ram_mb"`
87+ GPUVendor string `json:"gpu_vendor"`
88+ GPUName string `json:"gpu_name"`
89+ VRAMMB int `json:"vram_mb"`
90+}
91+
7992 type configPerformancePayload struct {
8093 Profile string `json:"profile"`
8194 Resolved configResolvedPerformance `json:"resolved"`
95+ Hardware configHardwarePayload `json:"hardware"`
8296 }
8397
8498 // configResolvedPerformance shows the actual values that will be applied at startup.
@@ -106,9 +120,11 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
106120 log := logging.FromContext(ctx)
107121
108122 h := &DumbSchemeHandler{
109- handlers: make(map[string]PageHandler),
110- assetDir: "webui",
111- logger: log.With().Str("component", "scheme-handler").Logger(),
123+ handlers: make(map[string]PageHandler),
124+ assetDir: "webui",
125+ logger: log.With().Str("component", "scheme-handler").Logger(),
126+ hwSurveyor: env.NewHardwareSurveyor(),
127+ ctx: ctx,
112128 }
113129
114130 // Register default pages
@@ -142,7 +158,7 @@ func (h *DumbSchemeHandler) registerDefaults() {
142158 return nil
143159 }
144160
145- return buildConfigResponse(config.Get())
161+ return h.buildConfigResponse(config.Get())
146162 }))
147163
148164 // API: Get default config (used by Reset Defaults in dumb://config)
@@ -151,13 +167,22 @@ func (h *DumbSchemeHandler) registerDefaults() {
151167 return nil
152168 }
153169
154- return buildConfigResponse(config.DefaultConfig())
170+ return h.buildConfigResponse(config.DefaultConfig())
155171 }))
156172 }
157173
158-func buildConfigResponse(cfg *config.Config) *SchemeResponse {
159- // Resolve performance profile to get actual values
160- resolved := config.ResolvePerformanceProfile(&cfg.Performance)
174+func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse {
175+ // Get hardware info for display and profile resolution
176+ // Use background context since survey results are cached and we don't want
177+ // request context cancellation to affect this
178+ var hw *port.HardwareInfo
179+ if h.hwSurveyor != nil {
180+ hwInfo := h.hwSurveyor.Survey(context.Background())
181+ hw = &hwInfo
182+ }
183+
184+ // Resolve performance profile with hardware info
185+ resolved := config.ResolvePerformanceProfile(&cfg.Performance, hw)
161186
162187 resp := configPayload{
163188 DefaultUIScale: cfg.DefaultUIScale,
@@ -184,6 +209,7 @@ func buildConfigResponse(cfg *config.Config) *SchemeResponse {
184209 StrictThreshold: resolved.WebProcessMemoryStrictThreshold,
185210 KillThreshold: resolved.WebProcessMemoryKillThreshold,
186211 },
212+ Hardware: buildHardwarePayload(hw),
187213 },
188214 }
189215
@@ -203,6 +229,21 @@ func buildConfigResponse(cfg *config.Config) *SchemeResponse {
203229 }
204230 }
205231
232+// buildHardwarePayload converts HardwareInfo to JSON payload.
233+func buildHardwarePayload(hw *port.HardwareInfo) configHardwarePayload {
234+ if hw == nil {
235+ return configHardwarePayload{}
236+ }
237+ return configHardwarePayload{
238+ CPUCores: hw.CPUCores,
239+ CPUThreads: hw.CPUThreads,
240+ TotalRAMMB: hw.TotalRAMMB(),
241+ GPUVendor: string(hw.GPUVendor),
242+ GPUName: hw.GPUName,
243+ VRAMMB: hw.VRAMMB(),
244+ }
245+}
246+
206247 // RegisterPage registers a handler for a specific path.
207248 func (h *DumbSchemeHandler) RegisterPage(path string, handler PageHandler) {
208249 h.mu.Lock()
@@ -13,7 +13,9 @@ import (
13 "strings"13 "strings"
14 "sync"14 "sync"
15 15
16+ "github.com/bnema/dumber/internal/application/port"
16 "github.com/bnema/dumber/internal/infrastructure/config"17 "github.com/bnema/dumber/internal/infrastructure/config"
18+ "github.com/bnema/dumber/internal/infrastructure/env"
17 "github.com/bnema/dumber/internal/logging"19 "github.com/bnema/dumber/internal/logging"
18 "github.com/bnema/puregotk-webkit/soup"20 "github.com/bnema/puregotk-webkit/soup"
19 "github.com/bnema/puregotk-webkit/webkit"21 "github.com/bnema/puregotk-webkit/webkit"
@@ -59,11 +61,13 @@ func (f PageHandlerFunc) Handle(req *SchemeRequest) *SchemeResponse {
59 61
60 // DumbSchemeHandler handles dumb:// URI scheme requests.62 // DumbSchemeHandler handles dumb:// URI scheme requests.
61 type DumbSchemeHandler struct {63 type DumbSchemeHandler struct {
62- handlers map[string]PageHandler64+ handlers map[string]PageHandler
63- assets embed.FS65+ assets embed.FS
64- assetDir string // subdirectory within embed.FS (e.g., "assets/webui")66+ assetDir string // subdirectory within embed.FS (e.g., "assets/webui")
65- logger zerolog.Logger67+ logger zerolog.Logger
66- mu sync.RWMutex68+ mu sync.RWMutex
69+ hwSurveyor *env.HardwareSurveyor
70+ ctx context.Context
67 }71 }
68 72
69 type configAppearancePayload struct {73 type configAppearancePayload struct {
@@ -76,9 +80,19 @@ type configAppearancePayload struct {
76 DarkPalette config.ColorPalette `json:"dark_palette"`80 DarkPalette config.ColorPalette `json:"dark_palette"`
77 }81 }
78 82
83+type configHardwarePayload struct {
84+ CPUCores int `json:"cpu_cores"`
85+ CPUThreads int `json:"cpu_threads"`
86+ TotalRAMMB int `json:"total_ram_mb"`
87+ GPUVendor string `json:"gpu_vendor"`
88+ GPUName string `json:"gpu_name"`
89+ VRAMMB int `json:"vram_mb"`
90+}
91+
79 type configPerformancePayload struct {92 type configPerformancePayload struct {
80 Profile string `json:"profile"`93 Profile string `json:"profile"`
81 Resolved configResolvedPerformance `json:"resolved"`94 Resolved configResolvedPerformance `json:"resolved"`
95+ Hardware configHardwarePayload `json:"hardware"`
82 }96 }
83 97
84 // configResolvedPerformance shows the actual values that will be applied at startup.98 // configResolvedPerformance shows the actual values that will be applied at startup.
@@ -106,9 +120,11 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
106 log := logging.FromContext(ctx)120 log := logging.FromContext(ctx)
107 121
108 h := &DumbSchemeHandler{122 h := &DumbSchemeHandler{
109- handlers: make(map[string]PageHandler),123+ handlers: make(map[string]PageHandler),
110- assetDir: "webui",124+ assetDir: "webui",
111- logger: log.With().Str("component", "scheme-handler").Logger(),125+ logger: log.With().Str("component", "scheme-handler").Logger(),
126+ hwSurveyor: env.NewHardwareSurveyor(),
127+ ctx: ctx,
112 }128 }
113 129
114 // Register default pages130 // Register default pages
@@ -142,7 +158,7 @@ func (h *DumbSchemeHandler) registerDefaults() {
142 return nil158 return nil
143 }159 }
144 160
145- return buildConfigResponse(config.Get())161+ return h.buildConfigResponse(config.Get())
146 }))162 }))
147 163
148 // API: Get default config (used by Reset Defaults in dumb://config)164 // API: Get default config (used by Reset Defaults in dumb://config)
@@ -151,13 +167,22 @@ func (h *DumbSchemeHandler) registerDefaults() {
151 return nil167 return nil
152 }168 }
153 169
154- return buildConfigResponse(config.DefaultConfig())170+ return h.buildConfigResponse(config.DefaultConfig())
155 }))171 }))
156 }172 }
157 173
158-func buildConfigResponse(cfg *config.Config) *SchemeResponse {174+func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse {
159- // Resolve performance profile to get actual values175+ // Get hardware info for display and profile resolution
160- resolved := config.ResolvePerformanceProfile(&cfg.Performance)176+ // Use background context since survey results are cached and we don't want
177+ // request context cancellation to affect this
178+ var hw *port.HardwareInfo
179+ if h.hwSurveyor != nil {
180+ hwInfo := h.hwSurveyor.Survey(context.Background())
181+ hw = &hwInfo
182+ }
183+
184+ // Resolve performance profile with hardware info
185+ resolved := config.ResolvePerformanceProfile(&cfg.Performance, hw)
161 186
162 resp := configPayload{187 resp := configPayload{
163 DefaultUIScale: cfg.DefaultUIScale,188 DefaultUIScale: cfg.DefaultUIScale,
@@ -184,6 +209,7 @@ func buildConfigResponse(cfg *config.Config) *SchemeResponse {
184 StrictThreshold: resolved.WebProcessMemoryStrictThreshold,209 StrictThreshold: resolved.WebProcessMemoryStrictThreshold,
185 KillThreshold: resolved.WebProcessMemoryKillThreshold,210 KillThreshold: resolved.WebProcessMemoryKillThreshold,
186 },211 },
212+ Hardware: buildHardwarePayload(hw),
187 },213 },
188 }214 }
189 215
@@ -203,6 +229,21 @@ func buildConfigResponse(cfg *config.Config) *SchemeResponse {
203 }229 }
204 }230 }
205 231
232+// buildHardwarePayload converts HardwareInfo to JSON payload.
233+func buildHardwarePayload(hw *port.HardwareInfo) configHardwarePayload {
234+ if hw == nil {
235+ return configHardwarePayload{}
236+ }
237+ return configHardwarePayload{
238+ CPUCores: hw.CPUCores,
239+ CPUThreads: hw.CPUThreads,
240+ TotalRAMMB: hw.TotalRAMMB(),
241+ GPUVendor: string(hw.GPUVendor),
242+ GPUName: hw.GPUName,
243+ VRAMMB: hw.VRAMMB(),
244+ }
245+}
246+
206 // RegisterPage registers a handler for a specific path.247 // RegisterPage registers a handler for a specific path.
207 func (h *DumbSchemeHandler) RegisterPage(path string, handler PageHandler) {248 func (h *DumbSchemeHandler) RegisterPage(path string, handler PageHandler) {
208 h.mu.Lock()249 h.mu.Lock()
M webui/src/pages/ConfigPage.svelte +27 -1
@@ -37,9 +37,19 @@
3737 kill_threshold: number;
3838 };
3939
40+ type HardwareInfo = {
41+ cpu_cores: number;
42+ cpu_threads: number;
43+ total_ram_mb: number;
44+ gpu_vendor: string;
45+ gpu_name: string;
46+ vram_mb: number;
47+ };
48+
4049 type PerformanceConfig = {
4150 profile: string;
4251 resolved: ResolvedPerformance;
52+ hardware: HardwareInfo;
4353 };
4454
4555 type ConfigDTO = {
@@ -61,7 +71,7 @@
6171 const PERFORMANCE_PROFILES = [
6272 { value: "default", label: "Default", description: "No tuning, uses WebKit defaults. Recommended for most users." },
6373 { value: "lite", label: "Lite", description: "Reduced resource usage for low-RAM systems (< 4GB) or battery saving." },
64- { value: "max", label: "Max", description: "Maximum responsiveness for heavy pages (GitHub PRs, complex SPAs)." },
74+ { value: "max", label: "Max", description: "Maximum responsiveness, scales based on detected hardware." },
6575 { value: "custom", label: "Custom", description: "Manual control via config file. Edit config.toml to set individual values." },
6676 ];
6777
@@ -415,6 +425,22 @@
415425 </Card.Description>
416426 </Card.Header>
417427 <Card.Content class="space-y-6">
428+ <!-- Detected Hardware Card -->
429+ {#if config.performance?.hardware}
430+ {@const hw = config.performance.hardware}
431+ <div class="rounded-md border border-border bg-muted/30 px-4 py-3">
432+ <div class="mb-2 text-xs font-semibold uppercase tracking-[0.15em] text-muted-foreground">
433+ Detected Hardware
434+ </div>
435+ <div class="flex flex-wrap gap-x-6 gap-y-1 text-sm">
436+ <div><span class="text-muted-foreground">CPU:</span> <span class="font-mono">{hw.cpu_cores}c/{hw.cpu_threads}t</span></div>
437+ <div><span class="text-muted-foreground">RAM:</span> <span class="font-mono">{hw.total_ram_mb > 0 ? (hw.total_ram_mb / 1024).toFixed(0) + ' GB' : '?'}</span></div>
438+ <div><span class="text-muted-foreground">GPU:</span> <span class="font-mono capitalize">{hw.gpu_vendor || 'Unknown'}</span></div>
439+ <div><span class="text-muted-foreground">VRAM:</span> <span class="font-mono">{hw.vram_mb > 0 ? (hw.vram_mb / 1024).toFixed(0) + ' GB' : '?'}</span></div>
440+ </div>
441+ </div>
442+ {/if}
443+
418444 <!-- Experimental feature warning -->
419445 <div class="flex items-start gap-3 rounded-md border border-info/30 bg-info/10 px-4 py-3">
420446 <span class="text-info">🧪</span>
@@ -37,9 +37,19 @@
37 kill_threshold: number;37 kill_threshold: number;
38 };38 };
39 39
40+ type HardwareInfo = {
41+ cpu_cores: number;
42+ cpu_threads: number;
43+ total_ram_mb: number;
44+ gpu_vendor: string;
45+ gpu_name: string;
46+ vram_mb: number;
47+ };
48+
40 type PerformanceConfig = {49 type PerformanceConfig = {
41 profile: string;50 profile: string;
42 resolved: ResolvedPerformance;51 resolved: ResolvedPerformance;
52+ hardware: HardwareInfo;
43 };53 };
44 54
45 type ConfigDTO = {55 type ConfigDTO = {
@@ -61,7 +71,7 @@
61 const PERFORMANCE_PROFILES = [71 const PERFORMANCE_PROFILES = [
62 { value: "default", label: "Default", description: "No tuning, uses WebKit defaults. Recommended for most users." },72 { value: "default", label: "Default", description: "No tuning, uses WebKit defaults. Recommended for most users." },
63 { value: "lite", label: "Lite", description: "Reduced resource usage for low-RAM systems (< 4GB) or battery saving." },73 { value: "lite", label: "Lite", description: "Reduced resource usage for low-RAM systems (< 4GB) or battery saving." },
64- { value: "max", label: "Max", description: "Maximum responsiveness for heavy pages (GitHub PRs, complex SPAs)." },74+ { value: "max", label: "Max", description: "Maximum responsiveness, scales based on detected hardware." },
65 { value: "custom", label: "Custom", description: "Manual control via config file. Edit config.toml to set individual values." },75 { value: "custom", label: "Custom", description: "Manual control via config file. Edit config.toml to set individual values." },
66 ];76 ];
67 77
@@ -415,6 +425,22 @@
415 </Card.Description>425 </Card.Description>
416 </Card.Header>426 </Card.Header>
417 <Card.Content class="space-y-6">427 <Card.Content class="space-y-6">
428+ <!-- Detected Hardware Card -->
429+ {#if config.performance?.hardware}
430+ {@const hw = config.performance.hardware}
431+ <div class="rounded-md border border-border bg-muted/30 px-4 py-3">
432+ <div class="mb-2 text-xs font-semibold uppercase tracking-[0.15em] text-muted-foreground">
433+ Detected Hardware
434+ </div>
435+ <div class="flex flex-wrap gap-x-6 gap-y-1 text-sm">
436+ <div><span class="text-muted-foreground">CPU:</span> <span class="font-mono">{hw.cpu_cores}c/{hw.cpu_threads}t</span></div>
437+ <div><span class="text-muted-foreground">RAM:</span> <span class="font-mono">{hw.total_ram_mb > 0 ? (hw.total_ram_mb / 1024).toFixed(0) + ' GB' : '?'}</span></div>
438+ <div><span class="text-muted-foreground">GPU:</span> <span class="font-mono capitalize">{hw.gpu_vendor || 'Unknown'}</span></div>
439+ <div><span class="text-muted-foreground">VRAM:</span> <span class="font-mono">{hw.vram_mb > 0 ? (hw.vram_mb / 1024).toFixed(0) + ' GB' : '?'}</span></div>
440+ </div>
441+ </div>
442+ {/if}
443+
418 <!-- Experimental feature warning -->444 <!-- Experimental feature warning -->
419 <div class="flex items-start gap-3 rounded-md border border-info/30 bg-info/10 px-4 py-3">445 <div class="flex items-start gap-3 rounded-md border border-info/30 bg-info/10 px-4 py-3">
420 <span class="text-info">🧪</span>446 <span class="text-info">🧪</span>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close