refactor(config): add read boundary for systemviews
- commit
- 86449d3bf4c6096b3648a832b0383a68e197204c
- author
- brice · · 2026-04-11 17:57 UTC
- committer
- brice · · 2026-04-12 04:52 UTC
- parent
- 8cb9bde
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +338 -123
M internal/application/port/webui_config.go +54 -0
| @@ -11,6 +11,54 @@ type WebUIConfig struct { | |||
| 11 | 11 | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | |
| 12 | 12 | } | |
| 13 | 13 | ||
| 14 | + | // SystemviewCustomPerformancePayload is the editable custom performance profile shape. | |
| 15 | + | type SystemviewCustomPerformancePayload struct { | |
| 16 | + | SkiaCPUThreads int `json:"skia_cpu_threads"` | |
| 17 | + | SkiaGPUThreads int `json:"skia_gpu_threads"` | |
| 18 | + | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |
| 19 | + | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |
| 20 | + | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // SystemviewResolvedPerformancePayload is the resolved effective performance shape. | |
| 24 | + | type SystemviewResolvedPerformancePayload struct { | |
| 25 | + | SkiaCPUThreads int `json:"skia_cpu_threads"` | |
| 26 | + | SkiaGPUThreads int `json:"skia_gpu_threads"` | |
| 27 | + | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |
| 28 | + | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |
| 29 | + | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |
| 30 | + | ConservativeThreshold float64 `json:"conservative_threshold"` | |
| 31 | + | StrictThreshold float64 `json:"strict_threshold"` | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // SystemviewHardwarePayload exposes detected hardware to the settings UI. | |
| 35 | + | type SystemviewHardwarePayload struct { | |
| 36 | + | CPUCores int `json:"cpu_cores"` | |
| 37 | + | CPUThreads int `json:"cpu_threads"` | |
| 38 | + | TotalRAMMB int `json:"total_ram_mb"` | |
| 39 | + | GPUVendor string `json:"gpu_vendor"` | |
| 40 | + | GPUName string `json:"gpu_name"` | |
| 41 | + | VRAMMB int `json:"vram_mb"` | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | // SystemviewPerformancePayload is the performance tab payload expected by the UI. | |
| 45 | + | type SystemviewPerformancePayload struct { | |
| 46 | + | Profile string `json:"profile"` | |
| 47 | + | Custom SystemviewCustomPerformancePayload `json:"custom"` | |
| 48 | + | Resolved SystemviewResolvedPerformancePayload `json:"resolved"` | |
| 49 | + | Hardware SystemviewHardwarePayload `json:"hardware"` | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // SystemviewConfigPayload is the full JSON shape expected by dumb://config. | |
| 53 | + | type SystemviewConfigPayload struct { | |
| 54 | + | Appearance WebUIAppearanceConfig `json:"appearance"` | |
| 55 | + | Performance SystemviewPerformancePayload `json:"performance"` | |
| 56 | + | DefaultUIScale float64 `json:"default_ui_scale"` | |
| 57 | + | DefaultSearchEngine string `json:"default_search_engine"` | |
| 58 | + | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | |
| 59 | + | EngineType string `json:"engine_type"` | |
| 60 | + | } | |
| 61 | + | ||
| 14 | 62 | // WebUIPerformanceConfig represents performance settings editable in dumb://config. | |
| 15 | 63 | type WebUIPerformanceConfig struct { | |
| 16 | 64 | Profile string `json:"profile"` | |
| @@ -51,6 +99,12 @@ type SearchShortcut struct { | |||
| 51 | 99 | Description string `json:"description"` | |
| 52 | 100 | } | |
| 53 | 101 | ||
| 102 | + | // SystemviewConfigReader reads the current and default systemview config payloads. | |
| 103 | + | type SystemviewConfigReader interface { | |
| 104 | + | Current(ctx context.Context) (SystemviewConfigPayload, error) | |
| 105 | + | Default(ctx context.Context) (SystemviewConfigPayload, error) | |
| 106 | + | } | |
| 107 | + | ||
| 54 | 108 | // WebUIConfigSaver persists WebUI configuration changes. | |
| 55 | 109 | type WebUIConfigSaver interface { | |
| 56 | 110 | SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error | |
| @@ -11,6 +11,54 @@ type WebUIConfig struct { | |||||
| 11 | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | 11 | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | ||
| 12 | } | 12 | } | ||
| 13 | 13 | ||||
| 14 | + | // SystemviewCustomPerformancePayload is the editable custom performance profile shape. | |||
| 15 | + | type SystemviewCustomPerformancePayload struct { | |||
| 16 | + | SkiaCPUThreads int `json:"skia_cpu_threads"` | |||
| 17 | + | SkiaGPUThreads int `json:"skia_gpu_threads"` | |||
| 18 | + | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |||
| 19 | + | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |||
| 20 | + | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // SystemviewResolvedPerformancePayload is the resolved effective performance shape. | |||
| 24 | + | type SystemviewResolvedPerformancePayload struct { | |||
| 25 | + | SkiaCPUThreads int `json:"skia_cpu_threads"` | |||
| 26 | + | SkiaGPUThreads int `json:"skia_gpu_threads"` | |||
| 27 | + | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |||
| 28 | + | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |||
| 29 | + | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |||
| 30 | + | ConservativeThreshold float64 `json:"conservative_threshold"` | |||
| 31 | + | StrictThreshold float64 `json:"strict_threshold"` | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // SystemviewHardwarePayload exposes detected hardware to the settings UI. | |||
| 35 | + | type SystemviewHardwarePayload struct { | |||
| 36 | + | CPUCores int `json:"cpu_cores"` | |||
| 37 | + | CPUThreads int `json:"cpu_threads"` | |||
| 38 | + | TotalRAMMB int `json:"total_ram_mb"` | |||
| 39 | + | GPUVendor string `json:"gpu_vendor"` | |||
| 40 | + | GPUName string `json:"gpu_name"` | |||
| 41 | + | VRAMMB int `json:"vram_mb"` | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | // SystemviewPerformancePayload is the performance tab payload expected by the UI. | |||
| 45 | + | type SystemviewPerformancePayload struct { | |||
| 46 | + | Profile string `json:"profile"` | |||
| 47 | + | Custom SystemviewCustomPerformancePayload `json:"custom"` | |||
| 48 | + | Resolved SystemviewResolvedPerformancePayload `json:"resolved"` | |||
| 49 | + | Hardware SystemviewHardwarePayload `json:"hardware"` | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // SystemviewConfigPayload is the full JSON shape expected by dumb://config. | |||
| 53 | + | type SystemviewConfigPayload struct { | |||
| 54 | + | Appearance WebUIAppearanceConfig `json:"appearance"` | |||
| 55 | + | Performance SystemviewPerformancePayload `json:"performance"` | |||
| 56 | + | DefaultUIScale float64 `json:"default_ui_scale"` | |||
| 57 | + | DefaultSearchEngine string `json:"default_search_engine"` | |||
| 58 | + | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | |||
| 59 | + | EngineType string `json:"engine_type"` | |||
| 60 | + | } | |||
| 61 | + | ||||
| 14 | // WebUIPerformanceConfig represents performance settings editable in dumb://config. | 62 | // WebUIPerformanceConfig represents performance settings editable in dumb://config. | ||
| 15 | type WebUIPerformanceConfig struct { | 63 | type WebUIPerformanceConfig struct { | ||
| 16 | Profile string `json:"profile"` | 64 | Profile string `json:"profile"` | ||
| @@ -51,6 +99,12 @@ type SearchShortcut struct { | |||||
| 51 | Description string `json:"description"` | 99 | Description string `json:"description"` | ||
| 52 | } | 100 | } | ||
| 53 | 101 | ||||
| 102 | + | // SystemviewConfigReader reads the current and default systemview config payloads. | |||
| 103 | + | type SystemviewConfigReader interface { | |||
| 104 | + | Current(ctx context.Context) (SystemviewConfigPayload, error) | |||
| 105 | + | Default(ctx context.Context) (SystemviewConfigPayload, error) | |||
| 106 | + | } | |||
| 107 | + | ||||
| 54 | // WebUIConfigSaver persists WebUI configuration changes. | 108 | // WebUIConfigSaver persists WebUI configuration changes. | ||
| 55 | type WebUIConfigSaver interface { | 109 | type WebUIConfigSaver interface { | ||
| 56 | SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error | 110 | SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error | ||
A internal/application/usecase/read_systemview_config.go +34 -0
| @@ -0,0 +1,34 @@ | |||
| 1 | + | package usecase | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // ReadSystemviewConfigUseCase exposes read-side systemview config payloads. | |
| 11 | + | type ReadSystemviewConfigUseCase struct { | |
| 12 | + | reader port.SystemviewConfigReader | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | // NewReadSystemviewConfigUseCase creates a new read systemview config use case. | |
| 16 | + | func NewReadSystemviewConfigUseCase(reader port.SystemviewConfigReader) *ReadSystemviewConfigUseCase { | |
| 17 | + | return &ReadSystemviewConfigUseCase{reader: reader} | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // Current returns the current systemview config payload. | |
| 21 | + | func (uc *ReadSystemviewConfigUseCase) Current(ctx context.Context) (port.SystemviewConfigPayload, error) { | |
| 22 | + | if uc == nil || uc.reader == nil { | |
| 23 | + | return port.SystemviewConfigPayload{}, fmt.Errorf("systemview config reader is nil") | |
| 24 | + | } | |
| 25 | + | return uc.reader.Current(ctx) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Default returns the default systemview config payload. | |
| 29 | + | func (uc *ReadSystemviewConfigUseCase) Default(ctx context.Context) (port.SystemviewConfigPayload, error) { | |
| 30 | + | if uc == nil || uc.reader == nil { | |
| 31 | + | return port.SystemviewConfigPayload{}, fmt.Errorf("systemview config reader is nil") | |
| 32 | + | } | |
| 33 | + | return uc.reader.Default(ctx) | |
| 34 | + | } | |
| @@ -0,0 +1,34 @@ | |||||
| 1 | + | package usecase | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // ReadSystemviewConfigUseCase exposes read-side systemview config payloads. | |||
| 11 | + | type ReadSystemviewConfigUseCase struct { | |||
| 12 | + | reader port.SystemviewConfigReader | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | // NewReadSystemviewConfigUseCase creates a new read systemview config use case. | |||
| 16 | + | func NewReadSystemviewConfigUseCase(reader port.SystemviewConfigReader) *ReadSystemviewConfigUseCase { | |||
| 17 | + | return &ReadSystemviewConfigUseCase{reader: reader} | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // Current returns the current systemview config payload. | |||
| 21 | + | func (uc *ReadSystemviewConfigUseCase) Current(ctx context.Context) (port.SystemviewConfigPayload, error) { | |||
| 22 | + | if uc == nil || uc.reader == nil { | |||
| 23 | + | return port.SystemviewConfigPayload{}, fmt.Errorf("systemview config reader is nil") | |||
| 24 | + | } | |||
| 25 | + | return uc.reader.Current(ctx) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // Default returns the default systemview config payload. | |||
| 29 | + | func (uc *ReadSystemviewConfigUseCase) Default(ctx context.Context) (port.SystemviewConfigPayload, error) { | |||
| 30 | + | if uc == nil || uc.reader == nil { | |||
| 31 | + | return port.SystemviewConfigPayload{}, fmt.Errorf("systemview config reader is nil") | |||
| 32 | + | } | |||
| 33 | + | return uc.reader.Default(ctx) | |||
| 34 | + | } | |||
A internal/application/usecase/read_systemview_config_test.go +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | package usecase | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 8 | + | "github.com/stretchr/testify/require" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestReadSystemviewConfigUseCase_Current(t *testing.T) { | |
| 12 | + | reader := &stubSystemviewConfigReader{ | |
| 13 | + | current: port.SystemviewConfigPayload{EngineType: "webkit"}, | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | uc := NewReadSystemviewConfigUseCase(reader) | |
| 17 | + | ||
| 18 | + | got, err := uc.Current(context.Background()) | |
| 19 | + | require.NoError(t, err) | |
| 20 | + | require.Equal(t, reader.current, got) | |
| 21 | + | require.Equal(t, []string{"current"}, reader.calls) | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func TestReadSystemviewConfigUseCase_Default(t *testing.T) { | |
| 25 | + | reader := &stubSystemviewConfigReader{ | |
| 26 | + | defaultPayload: port.SystemviewConfigPayload{EngineType: "webkit"}, | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | uc := NewReadSystemviewConfigUseCase(reader) | |
| 30 | + | ||
| 31 | + | got, err := uc.Default(context.Background()) | |
| 32 | + | require.NoError(t, err) | |
| 33 | + | require.Equal(t, reader.defaultPayload, got) | |
| 34 | + | require.Equal(t, []string{"default"}, reader.calls) | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | type stubSystemviewConfigReader struct { | |
| 38 | + | current port.SystemviewConfigPayload | |
| 39 | + | defaultPayload port.SystemviewConfigPayload | |
| 40 | + | calls []string | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (r *stubSystemviewConfigReader) Current(context.Context) (port.SystemviewConfigPayload, error) { | |
| 44 | + | r.calls = append(r.calls, "current") | |
| 45 | + | return r.current, nil | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func (r *stubSystemviewConfigReader) Default(context.Context) (port.SystemviewConfigPayload, error) { | |
| 49 | + | r.calls = append(r.calls, "default") | |
| 50 | + | return r.defaultPayload, nil | |
| 51 | + | } | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | package usecase | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 8 | + | "github.com/stretchr/testify/require" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestReadSystemviewConfigUseCase_Current(t *testing.T) { | |||
| 12 | + | reader := &stubSystemviewConfigReader{ | |||
| 13 | + | current: port.SystemviewConfigPayload{EngineType: "webkit"}, | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | uc := NewReadSystemviewConfigUseCase(reader) | |||
| 17 | + | ||||
| 18 | + | got, err := uc.Current(context.Background()) | |||
| 19 | + | require.NoError(t, err) | |||
| 20 | + | require.Equal(t, reader.current, got) | |||
| 21 | + | require.Equal(t, []string{"current"}, reader.calls) | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func TestReadSystemviewConfigUseCase_Default(t *testing.T) { | |||
| 25 | + | reader := &stubSystemviewConfigReader{ | |||
| 26 | + | defaultPayload: port.SystemviewConfigPayload{EngineType: "webkit"}, | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | uc := NewReadSystemviewConfigUseCase(reader) | |||
| 30 | + | ||||
| 31 | + | got, err := uc.Default(context.Background()) | |||
| 32 | + | require.NoError(t, err) | |||
| 33 | + | require.Equal(t, reader.defaultPayload, got) | |||
| 34 | + | require.Equal(t, []string{"default"}, reader.calls) | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | type stubSystemviewConfigReader struct { | |||
| 38 | + | current port.SystemviewConfigPayload | |||
| 39 | + | defaultPayload port.SystemviewConfigPayload | |||
| 40 | + | calls []string | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (r *stubSystemviewConfigReader) Current(context.Context) (port.SystemviewConfigPayload, error) { | |||
| 44 | + | r.calls = append(r.calls, "current") | |||
| 45 | + | return r.current, nil | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func (r *stubSystemviewConfigReader) Default(context.Context) (port.SystemviewConfigPayload, error) { | |||
| 49 | + | r.calls = append(r.calls, "default") | |||
| 50 | + | return r.defaultPayload, nil | |||
| 51 | + | } | |||
M internal/bootstrap/engine.go +30 -14
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/dumber/internal/application/port" | |
| 9 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | 10 | audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory" | |
| 10 | 11 | "github.com/bnema/dumber/internal/infrastructure/cef" | |
| 11 | 12 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| @@ -31,6 +32,19 @@ type EngineInput struct { | |||
| 31 | 32 | // BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type. | |
| 32 | 33 | func BuildEngine(input EngineInput) (port.Engine, error) { | |
| 33 | 34 | cfg := input.Config | |
| 35 | + | systemviewReader := config.NewSystemviewConfigReader(env.NewHardwareSurveyor()) | |
| 36 | + | systemviewUC := usecase.NewReadSystemviewConfigUseCase(systemviewReader) | |
| 37 | + | buildConfigPayload := func(read func(context.Context) (port.SystemviewConfigPayload, error)) func() ([]byte, error) { | |
| 38 | + | return func() ([]byte, error) { | |
| 39 | + | payload, err := read(input.Ctx) | |
| 40 | + | if err != nil { | |
| 41 | + | return nil, err | |
| 42 | + | } | |
| 43 | + | return json.Marshal(payload) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | currentConfigPayload := buildConfigPayload(systemviewUC.Current) | |
| 47 | + | defaultConfigPayload := buildConfigPayload(systemviewUC.Default) | |
| 34 | 48 | engineType := cfg.Engine.ResolveEngineType() | |
| 35 | 49 | switch engineType { | |
| 36 | 50 | case config.EngineTypeWebKit: | |
| @@ -40,9 +54,23 @@ func BuildEngine(input EngineInput) (port.Engine, error) { | |||
| 40 | 54 | CookiePolicy: port.CookiePolicy(cfg.Engine.CookiePolicy), | |
| 41 | 55 | } | |
| 42 | 56 | wkCfg := webkit.EngineConfigFromConfig(cfg.Engine.WebKit) | |
| 57 | + | systemviewReader := config.NewSystemviewConfigReader(env.NewHardwareSurveyor()) | |
| 58 | + | systemviewUC := usecase.NewReadSystemviewConfigUseCase(systemviewReader) | |
| 59 | + | buildConfigPayload := func(read func(context.Context) (port.SystemviewConfigPayload, error)) func() ([]byte, error) { | |
| 60 | + | return func() ([]byte, error) { | |
| 61 | + | payload, err := read(input.Ctx) | |
| 62 | + | if err != nil { | |
| 63 | + | return nil, err | |
| 64 | + | } | |
| 65 | + | return json.Marshal(payload) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | currentConfigPayload := buildConfigPayload(systemviewUC.Current) | |
| 69 | + | defaultConfigPayload := buildConfigPayload(systemviewUC.Default) | |
| 43 | 70 | ||
| 44 | 71 | return webkit.NewEngine( | |
| 45 | 72 | input.Ctx, cfg, opts, wkCfg, | |
| 73 | + | currentConfigPayload, defaultConfigPayload, | |
| 46 | 74 | input.ThemeManager, input.ColorResolver, input.Logger, | |
| 47 | 75 | ) | |
| 48 | 76 | case config.EngineTypeCEF: | |
| @@ -66,23 +94,11 @@ func BuildEngine(input EngineInput) (port.Engine, error) { | |||
| 66 | 94 | MaxConcurrent: cfg.Transcoding.MaxConcurrent, | |
| 67 | 95 | Quality: cfg.Transcoding.Quality, | |
| 68 | 96 | } | |
| 69 | - | surveyor := env.NewHardwareSurveyor() | |
| 70 | - | buildConfigPayload := func(cfgf func() *config.Config) func() ([]byte, error) { | |
| 71 | - | return func() ([]byte, error) { | |
| 72 | - | cfg := cfgf() | |
| 73 | - | var hw *port.HardwareInfo | |
| 74 | - | if surveyor != nil { | |
| 75 | - | survey := surveyor.Survey(context.Background()) | |
| 76 | - | hw = &survey | |
| 77 | - | } | |
| 78 | - | return json.Marshal(config.BuildWebUIConfigPayload(cfg, hw)) | |
| 79 | - | } | |
| 80 | - | } | |
| 81 | 97 | deps := cef.EngineDependencies{ | |
| 82 | 98 | RegisterHandlers: handlers.RegisterAll, | |
| 83 | 99 | RegisterAccentHandlers: handlers.RegisterAccentHandlers, | |
| 84 | - | CurrentConfigPayload: buildConfigPayload(config.Get), | |
| 85 | - | DefaultConfigPayload: buildConfigPayload(config.DefaultConfig), | |
| 100 | + | CurrentConfigPayload: currentConfigPayload, | |
| 101 | + | DefaultConfigPayload: defaultConfigPayload, | |
| 86 | 102 | MediaClassifier: cef.MediaClassifier{ | |
| 87 | 103 | IsProprietaryVideoMIME: transcoder.IsProprietaryVideoMIME, | |
| 88 | 104 | IsOpenVideoMIME: transcoder.IsOpenVideoMIME, | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | 7 | ||||
| 8 | "github.com/bnema/dumber/internal/application/port" | 8 | "github.com/bnema/dumber/internal/application/port" | ||
| 9 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 9 | audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory" | 10 | audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory" | ||
| 10 | "github.com/bnema/dumber/internal/infrastructure/cef" | 11 | "github.com/bnema/dumber/internal/infrastructure/cef" | ||
| 11 | "github.com/bnema/dumber/internal/infrastructure/config" | 12 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| @@ -31,6 +32,19 @@ type EngineInput struct { | |||||
| 31 | // BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type. | 32 | // BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type. | ||
| 32 | func BuildEngine(input EngineInput) (port.Engine, error) { | 33 | func BuildEngine(input EngineInput) (port.Engine, error) { | ||
| 33 | cfg := input.Config | 34 | cfg := input.Config | ||
| 35 | + | systemviewReader := config.NewSystemviewConfigReader(env.NewHardwareSurveyor()) | |||
| 36 | + | systemviewUC := usecase.NewReadSystemviewConfigUseCase(systemviewReader) | |||
| 37 | + | buildConfigPayload := func(read func(context.Context) (port.SystemviewConfigPayload, error)) func() ([]byte, error) { | |||
| 38 | + | return func() ([]byte, error) { | |||
| 39 | + | payload, err := read(input.Ctx) | |||
| 40 | + | if err != nil { | |||
| 41 | + | return nil, err | |||
| 42 | + | } | |||
| 43 | + | return json.Marshal(payload) | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | currentConfigPayload := buildConfigPayload(systemviewUC.Current) | |||
| 47 | + | defaultConfigPayload := buildConfigPayload(systemviewUC.Default) | |||
| 34 | engineType := cfg.Engine.ResolveEngineType() | 48 | engineType := cfg.Engine.ResolveEngineType() | ||
| 35 | switch engineType { | 49 | switch engineType { | ||
| 36 | case config.EngineTypeWebKit: | 50 | case config.EngineTypeWebKit: | ||
| @@ -40,9 +54,23 @@ func BuildEngine(input EngineInput) (port.Engine, error) { | |||||
| 40 | CookiePolicy: port.CookiePolicy(cfg.Engine.CookiePolicy), | 54 | CookiePolicy: port.CookiePolicy(cfg.Engine.CookiePolicy), | ||
| 41 | } | 55 | } | ||
| 42 | wkCfg := webkit.EngineConfigFromConfig(cfg.Engine.WebKit) | 56 | wkCfg := webkit.EngineConfigFromConfig(cfg.Engine.WebKit) | ||
| 57 | + | systemviewReader := config.NewSystemviewConfigReader(env.NewHardwareSurveyor()) | |||
| 58 | + | systemviewUC := usecase.NewReadSystemviewConfigUseCase(systemviewReader) | |||
| 59 | + | buildConfigPayload := func(read func(context.Context) (port.SystemviewConfigPayload, error)) func() ([]byte, error) { | |||
| 60 | + | return func() ([]byte, error) { | |||
| 61 | + | payload, err := read(input.Ctx) | |||
| 62 | + | if err != nil { | |||
| 63 | + | return nil, err | |||
| 64 | + | } | |||
| 65 | + | return json.Marshal(payload) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | currentConfigPayload := buildConfigPayload(systemviewUC.Current) | |||
| 69 | + | defaultConfigPayload := buildConfigPayload(systemviewUC.Default) | |||
| 43 | 70 | ||||
| 44 | return webkit.NewEngine( | 71 | return webkit.NewEngine( | ||
| 45 | input.Ctx, cfg, opts, wkCfg, | 72 | input.Ctx, cfg, opts, wkCfg, | ||
| 73 | + | currentConfigPayload, defaultConfigPayload, | |||
| 46 | input.ThemeManager, input.ColorResolver, input.Logger, | 74 | input.ThemeManager, input.ColorResolver, input.Logger, | ||
| 47 | ) | 75 | ) | ||
| 48 | case config.EngineTypeCEF: | 76 | case config.EngineTypeCEF: | ||
| @@ -66,23 +94,11 @@ func BuildEngine(input EngineInput) (port.Engine, error) { | |||||
| 66 | MaxConcurrent: cfg.Transcoding.MaxConcurrent, | 94 | MaxConcurrent: cfg.Transcoding.MaxConcurrent, | ||
| 67 | Quality: cfg.Transcoding.Quality, | 95 | Quality: cfg.Transcoding.Quality, | ||
| 68 | } | 96 | } | ||
| 69 | - | surveyor := env.NewHardwareSurveyor() | |||
| 70 | - | buildConfigPayload := func(cfgf func() *config.Config) func() ([]byte, error) { | |||
| 71 | - | return func() ([]byte, error) { | |||
| 72 | - | cfg := cfgf() | |||
| 73 | - | var hw *port.HardwareInfo | |||
| 74 | - | if surveyor != nil { | |||
| 75 | - | survey := surveyor.Survey(context.Background()) | |||
| 76 | - | hw = &survey | |||
| 77 | - | } | |||
| 78 | - | return json.Marshal(config.BuildWebUIConfigPayload(cfg, hw)) | |||
| 79 | - | } | |||
| 80 | - | } | |||
| 81 | deps := cef.EngineDependencies{ | 97 | deps := cef.EngineDependencies{ | ||
| 82 | RegisterHandlers: handlers.RegisterAll, | 98 | RegisterHandlers: handlers.RegisterAll, | ||
| 83 | RegisterAccentHandlers: handlers.RegisterAccentHandlers, | 99 | RegisterAccentHandlers: handlers.RegisterAccentHandlers, | ||
| 84 | - | CurrentConfigPayload: buildConfigPayload(config.Get), | 100 | + | CurrentConfigPayload: currentConfigPayload, |
| 85 | - | DefaultConfigPayload: buildConfigPayload(config.DefaultConfig), | 101 | + | DefaultConfigPayload: defaultConfigPayload, |
| 86 | MediaClassifier: cef.MediaClassifier{ | 102 | MediaClassifier: cef.MediaClassifier{ | ||
| 87 | IsProprietaryVideoMIME: transcoder.IsProprietaryVideoMIME, | 103 | IsProprietaryVideoMIME: transcoder.IsProprietaryVideoMIME, | ||
| 88 | IsOpenVideoMIME: transcoder.IsOpenVideoMIME, | 104 | IsOpenVideoMIME: transcoder.IsOpenVideoMIME, | ||
A internal/infrastructure/config/systemview_reader.go +40 -0
| @@ -0,0 +1,40 @@ | |||
| 1 | + | package config | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | type systemviewConfigReader struct { | |
| 10 | + | hwSurveyor port.HardwareSurveyor | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | var _ port.SystemviewConfigReader = (*systemviewConfigReader)(nil) | |
| 14 | + | ||
| 15 | + | // NewSystemviewConfigReader creates a read-side config payload reader. | |
| 16 | + | func NewSystemviewConfigReader(hwSurveyor port.HardwareSurveyor) port.SystemviewConfigReader { | |
| 17 | + | return &systemviewConfigReader{hwSurveyor: hwSurveyor} | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func (r *systemviewConfigReader) Current(ctx context.Context) (port.SystemviewConfigPayload, error) { | |
| 21 | + | return r.build(ctx, Get()) | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func (r *systemviewConfigReader) Default(ctx context.Context) (port.SystemviewConfigPayload, error) { | |
| 25 | + | return r.build(ctx, DefaultConfig()) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func (r *systemviewConfigReader) build(ctx context.Context, cfg *Config) (port.SystemviewConfigPayload, error) { | |
| 29 | + | if cfg == nil { | |
| 30 | + | return port.SystemviewConfigPayload{}, nil | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | var hw *port.HardwareInfo | |
| 34 | + | if r != nil && r.hwSurveyor != nil { | |
| 35 | + | survey := r.hwSurveyor.Survey(ctx) | |
| 36 | + | hw = &survey | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | return BuildSystemviewConfigPayload(cfg, hw), nil | |
| 40 | + | } | |
| @@ -0,0 +1,40 @@ | |||||
| 1 | + | package config | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | type systemviewConfigReader struct { | |||
| 10 | + | hwSurveyor port.HardwareSurveyor | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | var _ port.SystemviewConfigReader = (*systemviewConfigReader)(nil) | |||
| 14 | + | ||||
| 15 | + | // NewSystemviewConfigReader creates a read-side config payload reader. | |||
| 16 | + | func NewSystemviewConfigReader(hwSurveyor port.HardwareSurveyor) port.SystemviewConfigReader { | |||
| 17 | + | return &systemviewConfigReader{hwSurveyor: hwSurveyor} | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func (r *systemviewConfigReader) Current(ctx context.Context) (port.SystemviewConfigPayload, error) { | |||
| 21 | + | return r.build(ctx, Get()) | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func (r *systemviewConfigReader) Default(ctx context.Context) (port.SystemviewConfigPayload, error) { | |||
| 25 | + | return r.build(ctx, DefaultConfig()) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func (r *systemviewConfigReader) build(ctx context.Context, cfg *Config) (port.SystemviewConfigPayload, error) { | |||
| 29 | + | if cfg == nil { | |||
| 30 | + | return port.SystemviewConfigPayload{}, nil | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | var hw *port.HardwareInfo | |||
| 34 | + | if r != nil && r.hwSurveyor != nil { | |||
| 35 | + | survey := r.hwSurveyor.Survey(ctx) | |||
| 36 | + | hw = &survey | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | return BuildSystemviewConfigPayload(cfg, hw), nil | |||
| 40 | + | } | |||
M internal/infrastructure/config/webui_payload.go +53 -80
| @@ -2,99 +2,32 @@ package config | |||
| 2 | 2 | ||
| 3 | 3 | import "github.com/bnema/dumber/internal/application/port" | |
| 4 | 4 | ||
| 5 | - | // WebUIConfigAppearancePayload is the JSON shape expected by dumb://config. | |
| 6 | - | type WebUIConfigAppearancePayload struct { | |
| 7 | - | SansFont string `json:"sans_font"` | |
| 8 | - | SerifFont string `json:"serif_font"` | |
| 9 | - | MonospaceFont string `json:"monospace_font"` | |
| 10 | - | DefaultFontSize int `json:"default_font_size"` | |
| 11 | - | ColorScheme string `json:"color_scheme"` | |
| 12 | - | LightPalette ColorPalette `json:"light_palette"` | |
| 13 | - | DarkPalette ColorPalette `json:"dark_palette"` | |
| 14 | - | } | |
| 15 | - | ||
| 16 | - | // WebUIConfigHardwarePayload exposes detected hardware to the settings UI. | |
| 17 | - | type WebUIConfigHardwarePayload struct { | |
| 18 | - | CPUCores int `json:"cpu_cores"` | |
| 19 | - | CPUThreads int `json:"cpu_threads"` | |
| 20 | - | TotalRAMMB int `json:"total_ram_mb"` | |
| 21 | - | GPUVendor string `json:"gpu_vendor"` | |
| 22 | - | GPUName string `json:"gpu_name"` | |
| 23 | - | VRAMMB int `json:"vram_mb"` | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | // WebUIConfigCustomPerformancePayload holds user-editable custom profile values. | |
| 27 | - | type WebUIConfigCustomPerformancePayload struct { | |
| 28 | - | SkiaCPUThreads int `json:"skia_cpu_threads"` | |
| 29 | - | SkiaGPUThreads int `json:"skia_gpu_threads"` | |
| 30 | - | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |
| 31 | - | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |
| 32 | - | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |
| 33 | - | } | |
| 34 | - | ||
| 35 | - | // WebUIConfigResolvedPerformancePayload holds the resolved effective values. | |
| 36 | - | type WebUIConfigResolvedPerformancePayload struct { | |
| 37 | - | SkiaCPUThreads int `json:"skia_cpu_threads"` | |
| 38 | - | SkiaGPUThreads int `json:"skia_gpu_threads"` | |
| 39 | - | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |
| 40 | - | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |
| 41 | - | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |
| 42 | - | ConservativeThreshold float64 `json:"conservative_threshold"` | |
| 43 | - | StrictThreshold float64 `json:"strict_threshold"` | |
| 44 | - | } | |
| 45 | - | ||
| 46 | - | // WebUIConfigPerformancePayload is the performance tab payload expected by the UI. | |
| 47 | - | type WebUIConfigPerformancePayload struct { | |
| 48 | - | Profile string `json:"profile"` | |
| 49 | - | Custom WebUIConfigCustomPerformancePayload `json:"custom"` | |
| 50 | - | Resolved WebUIConfigResolvedPerformancePayload `json:"resolved"` | |
| 51 | - | Hardware WebUIConfigHardwarePayload `json:"hardware"` | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | // WebUIConfigPayload is the full JSON shape expected by dumb://config. | |
| 55 | - | type WebUIConfigPayload struct { | |
| 56 | - | Appearance WebUIConfigAppearancePayload `json:"appearance"` | |
| 57 | - | Performance WebUIConfigPerformancePayload `json:"performance"` | |
| 58 | - | DefaultUIScale float64 `json:"default_ui_scale"` | |
| 59 | - | DefaultSearchEngine string `json:"default_search_engine"` | |
| 60 | - | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | |
| 61 | - | EngineType string `json:"engine_type"` | |
| 62 | - | } | |
| 63 | - | ||
| 64 | - | // BuildWebUIConfigPayload projects the full config into the stable DTO used by | |
| 5 | + | // BuildSystemviewConfigPayload projects the full config into the stable DTO used by | |
| 65 | 6 | // the settings page across engines. | |
| 66 | - | func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayload { | |
| 7 | + | func BuildSystemviewConfigPayload(cfg *Config, hw *port.HardwareInfo) port.SystemviewConfigPayload { | |
| 67 | 8 | if cfg == nil { | |
| 68 | - | return WebUIConfigPayload{} | |
| 9 | + | return port.SystemviewConfigPayload{} | |
| 69 | 10 | } | |
| 70 | 11 | ||
| 71 | 12 | perfCfg := PerformanceConfigFromEngine(&cfg.Engine) | |
| 72 | 13 | resolved := ResolvePerformanceProfile(&perfCfg, hw) | |
| 73 | 14 | ||
| 74 | - | return WebUIConfigPayload{ | |
| 15 | + | return port.SystemviewConfigPayload{ | |
| 16 | + | Appearance: buildSystemviewAppearancePayload(cfg.Appearance), | |
| 75 | 17 | DefaultUIScale: cfg.DefaultUIScale, | |
| 76 | 18 | DefaultSearchEngine: cfg.DefaultSearchEngine, | |
| 77 | - | SearchShortcuts: cfg.SearchShortcuts, | |
| 19 | + | SearchShortcuts: buildSystemviewSearchShortcutsPayload(cfg.SearchShortcuts), | |
| 78 | 20 | EngineType: cfg.Engine.ResolveEngineType(), | |
| 79 | - | Appearance: WebUIConfigAppearancePayload{ | |
| 80 | - | SansFont: cfg.Appearance.SansFont, | |
| 81 | - | SerifFont: cfg.Appearance.SerifFont, | |
| 82 | - | MonospaceFont: cfg.Appearance.MonospaceFont, | |
| 83 | - | DefaultFontSize: cfg.Appearance.DefaultFontSize, | |
| 84 | - | ColorScheme: cfg.Appearance.ColorScheme, | |
| 85 | - | LightPalette: cfg.Appearance.LightPalette, | |
| 86 | - | DarkPalette: cfg.Appearance.DarkPalette, | |
| 87 | - | }, | |
| 88 | - | Performance: WebUIConfigPerformancePayload{ | |
| 21 | + | Performance: port.SystemviewPerformancePayload{ | |
| 89 | 22 | Profile: string(cfg.Engine.Profile), | |
| 90 | - | Custom: WebUIConfigCustomPerformancePayload{ | |
| 23 | + | Custom: port.SystemviewCustomPerformancePayload{ | |
| 91 | 24 | SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads, | |
| 92 | 25 | SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads, | |
| 93 | 26 | WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB, | |
| 94 | 27 | NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB, | |
| 95 | 28 | WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount, | |
| 96 | 29 | }, | |
| 97 | - | Resolved: WebUIConfigResolvedPerformancePayload{ | |
| 30 | + | Resolved: port.SystemviewResolvedPerformancePayload{ | |
| 98 | 31 | SkiaCPUThreads: resolved.SkiaCPUPaintingThreads, | |
| 99 | 32 | SkiaGPUThreads: resolved.SkiaGPUPaintingThreads, | |
| 100 | 33 | WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB, | |
| @@ -103,16 +36,56 @@ func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayl | |||
| 103 | 36 | ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold, | |
| 104 | 37 | StrictThreshold: resolved.WebProcessMemoryStrictThreshold, | |
| 105 | 38 | }, | |
| 106 | - | Hardware: buildWebUIHardwarePayload(hw), | |
| 39 | + | Hardware: buildSystemviewHardwarePayload(hw), | |
| 40 | + | }, | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func buildSystemviewAppearancePayload(appearance AppearanceConfig) port.WebUIAppearanceConfig { | |
| 45 | + | return port.WebUIAppearanceConfig{ | |
| 46 | + | SansFont: appearance.SansFont, | |
| 47 | + | SerifFont: appearance.SerifFont, | |
| 48 | + | MonospaceFont: appearance.MonospaceFont, | |
| 49 | + | DefaultFontSize: appearance.DefaultFontSize, | |
| 50 | + | ColorScheme: appearance.ColorScheme, | |
| 51 | + | LightPalette: port.ColorPalette{ | |
| 52 | + | Background: appearance.LightPalette.Background, | |
| 53 | + | Surface: appearance.LightPalette.Surface, | |
| 54 | + | SurfaceVariant: appearance.LightPalette.SurfaceVariant, | |
| 55 | + | Text: appearance.LightPalette.Text, | |
| 56 | + | Muted: appearance.LightPalette.Muted, | |
| 57 | + | Accent: appearance.LightPalette.Accent, | |
| 58 | + | Border: appearance.LightPalette.Border, | |
| 59 | + | }, | |
| 60 | + | DarkPalette: port.ColorPalette{ | |
| 61 | + | Background: appearance.DarkPalette.Background, | |
| 62 | + | Surface: appearance.DarkPalette.Surface, | |
| 63 | + | SurfaceVariant: appearance.DarkPalette.SurfaceVariant, | |
| 64 | + | Text: appearance.DarkPalette.Text, | |
| 65 | + | Muted: appearance.DarkPalette.Muted, | |
| 66 | + | Accent: appearance.DarkPalette.Accent, | |
| 67 | + | Border: appearance.DarkPalette.Border, | |
| 107 | 68 | }, | |
| 108 | 69 | } | |
| 109 | 70 | } | |
| 110 | 71 | ||
| 111 | - | func buildWebUIHardwarePayload(hw *port.HardwareInfo) WebUIConfigHardwarePayload { | |
| 72 | + | func buildSystemviewSearchShortcutsPayload(shortcuts map[string]SearchShortcut) map[string]port.SearchShortcut { | |
| 73 | + | if len(shortcuts) == 0 { | |
| 74 | + | return nil | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | result := make(map[string]port.SearchShortcut, len(shortcuts)) | |
| 78 | + | for key, shortcut := range shortcuts { | |
| 79 | + | result[key] = port.SearchShortcut{URL: shortcut.URL, Description: shortcut.Description} | |
| 80 | + | } | |
| 81 | + | return result | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | func buildSystemviewHardwarePayload(hw *port.HardwareInfo) port.SystemviewHardwarePayload { | |
| 112 | 85 | if hw == nil { | |
| 113 | - | return WebUIConfigHardwarePayload{} | |
| 86 | + | return port.SystemviewHardwarePayload{} | |
| 114 | 87 | } | |
| 115 | - | return WebUIConfigHardwarePayload{ | |
| 88 | + | return port.SystemviewHardwarePayload{ | |
| 116 | 89 | CPUCores: hw.CPUCores, | |
| 117 | 90 | CPUThreads: hw.CPUThreads, | |
| 118 | 91 | TotalRAMMB: hw.TotalRAMMB(), | |
| @@ -2,99 +2,32 @@ package config | |||||
| 2 | 2 | ||||
| 3 | import "github.com/bnema/dumber/internal/application/port" | 3 | import "github.com/bnema/dumber/internal/application/port" | ||
| 4 | 4 | ||||
| 5 | - | // WebUIConfigAppearancePayload is the JSON shape expected by dumb://config. | 5 | + | // BuildSystemviewConfigPayload projects the full config into the stable DTO used by |
| 6 | - | type WebUIConfigAppearancePayload struct { | |||
| 7 | - | SansFont string `json:"sans_font"` | |||
| 8 | - | SerifFont string `json:"serif_font"` | |||
| 9 | - | MonospaceFont string `json:"monospace_font"` | |||
| 10 | - | DefaultFontSize int `json:"default_font_size"` | |||
| 11 | - | ColorScheme string `json:"color_scheme"` | |||
| 12 | - | LightPalette ColorPalette `json:"light_palette"` | |||
| 13 | - | DarkPalette ColorPalette `json:"dark_palette"` | |||
| 14 | - | } | |||
| 15 | - | ||||
| 16 | - | // WebUIConfigHardwarePayload exposes detected hardware to the settings UI. | |||
| 17 | - | type WebUIConfigHardwarePayload struct { | |||
| 18 | - | CPUCores int `json:"cpu_cores"` | |||
| 19 | - | CPUThreads int `json:"cpu_threads"` | |||
| 20 | - | TotalRAMMB int `json:"total_ram_mb"` | |||
| 21 | - | GPUVendor string `json:"gpu_vendor"` | |||
| 22 | - | GPUName string `json:"gpu_name"` | |||
| 23 | - | VRAMMB int `json:"vram_mb"` | |||
| 24 | - | } | |||
| 25 | - | ||||
| 26 | - | // WebUIConfigCustomPerformancePayload holds user-editable custom profile values. | |||
| 27 | - | type WebUIConfigCustomPerformancePayload struct { | |||
| 28 | - | SkiaCPUThreads int `json:"skia_cpu_threads"` | |||
| 29 | - | SkiaGPUThreads int `json:"skia_gpu_threads"` | |||
| 30 | - | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |||
| 31 | - | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |||
| 32 | - | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |||
| 33 | - | } | |||
| 34 | - | ||||
| 35 | - | // WebUIConfigResolvedPerformancePayload holds the resolved effective values. | |||
| 36 | - | type WebUIConfigResolvedPerformancePayload struct { | |||
| 37 | - | SkiaCPUThreads int `json:"skia_cpu_threads"` | |||
| 38 | - | SkiaGPUThreads int `json:"skia_gpu_threads"` | |||
| 39 | - | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |||
| 40 | - | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |||
| 41 | - | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |||
| 42 | - | ConservativeThreshold float64 `json:"conservative_threshold"` | |||
| 43 | - | StrictThreshold float64 `json:"strict_threshold"` | |||
| 44 | - | } | |||
| 45 | - | ||||
| 46 | - | // WebUIConfigPerformancePayload is the performance tab payload expected by the UI. | |||
| 47 | - | type WebUIConfigPerformancePayload struct { | |||
| 48 | - | Profile string `json:"profile"` | |||
| 49 | - | Custom WebUIConfigCustomPerformancePayload `json:"custom"` | |||
| 50 | - | Resolved WebUIConfigResolvedPerformancePayload `json:"resolved"` | |||
| 51 | - | Hardware WebUIConfigHardwarePayload `json:"hardware"` | |||
| 52 | - | } | |||
| 53 | - | ||||
| 54 | - | // WebUIConfigPayload is the full JSON shape expected by dumb://config. | |||
| 55 | - | type WebUIConfigPayload struct { | |||
| 56 | - | Appearance WebUIConfigAppearancePayload `json:"appearance"` | |||
| 57 | - | Performance WebUIConfigPerformancePayload `json:"performance"` | |||
| 58 | - | DefaultUIScale float64 `json:"default_ui_scale"` | |||
| 59 | - | DefaultSearchEngine string `json:"default_search_engine"` | |||
| 60 | - | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | |||
| 61 | - | EngineType string `json:"engine_type"` | |||
| 62 | - | } | |||
| 63 | - | ||||
| 64 | - | // BuildWebUIConfigPayload projects the full config into the stable DTO used by | |||
| 65 | // the settings page across engines. | 6 | // the settings page across engines. | ||
| 66 | - | func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayload { | 7 | + | func BuildSystemviewConfigPayload(cfg *Config, hw *port.HardwareInfo) port.SystemviewConfigPayload { |
| 67 | if cfg == nil { | 8 | if cfg == nil { | ||
| 68 | - | return WebUIConfigPayload{} | 9 | + | return port.SystemviewConfigPayload{} |
| 69 | } | 10 | } | ||
| 70 | 11 | ||||
| 71 | perfCfg := PerformanceConfigFromEngine(&cfg.Engine) | 12 | perfCfg := PerformanceConfigFromEngine(&cfg.Engine) | ||
| 72 | resolved := ResolvePerformanceProfile(&perfCfg, hw) | 13 | resolved := ResolvePerformanceProfile(&perfCfg, hw) | ||
| 73 | 14 | ||||
| 74 | - | return WebUIConfigPayload{ | 15 | + | return port.SystemviewConfigPayload{ |
| 16 | + | Appearance: buildSystemviewAppearancePayload(cfg.Appearance), | |||
| 75 | DefaultUIScale: cfg.DefaultUIScale, | 17 | DefaultUIScale: cfg.DefaultUIScale, | ||
| 76 | DefaultSearchEngine: cfg.DefaultSearchEngine, | 18 | DefaultSearchEngine: cfg.DefaultSearchEngine, | ||
| 77 | - | SearchShortcuts: cfg.SearchShortcuts, | 19 | + | SearchShortcuts: buildSystemviewSearchShortcutsPayload(cfg.SearchShortcuts), |
| 78 | EngineType: cfg.Engine.ResolveEngineType(), | 20 | EngineType: cfg.Engine.ResolveEngineType(), | ||
| 79 | - | Appearance: WebUIConfigAppearancePayload{ | 21 | + | Performance: port.SystemviewPerformancePayload{ |
| 80 | - | SansFont: cfg.Appearance.SansFont, | |||
| 81 | - | SerifFont: cfg.Appearance.SerifFont, | |||
| 82 | - | MonospaceFont: cfg.Appearance.MonospaceFont, | |||
| 83 | - | DefaultFontSize: cfg.Appearance.DefaultFontSize, | |||
| 84 | - | ColorScheme: cfg.Appearance.ColorScheme, | |||
| 85 | - | LightPalette: cfg.Appearance.LightPalette, | |||
| 86 | - | DarkPalette: cfg.Appearance.DarkPalette, | |||
| 87 | - | }, | |||
| 88 | - | Performance: WebUIConfigPerformancePayload{ | |||
| 89 | Profile: string(cfg.Engine.Profile), | 22 | Profile: string(cfg.Engine.Profile), | ||
| 90 | - | Custom: WebUIConfigCustomPerformancePayload{ | 23 | + | Custom: port.SystemviewCustomPerformancePayload{ |
| 91 | SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads, | 24 | SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads, | ||
| 92 | SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads, | 25 | SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads, | ||
| 93 | WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB, | 26 | WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB, | ||
| 94 | NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB, | 27 | NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB, | ||
| 95 | WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount, | 28 | WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount, | ||
| 96 | }, | 29 | }, | ||
| 97 | - | Resolved: WebUIConfigResolvedPerformancePayload{ | 30 | + | Resolved: port.SystemviewResolvedPerformancePayload{ |
| 98 | SkiaCPUThreads: resolved.SkiaCPUPaintingThreads, | 31 | SkiaCPUThreads: resolved.SkiaCPUPaintingThreads, | ||
| 99 | SkiaGPUThreads: resolved.SkiaGPUPaintingThreads, | 32 | SkiaGPUThreads: resolved.SkiaGPUPaintingThreads, | ||
| 100 | WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB, | 33 | WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB, | ||
| @@ -103,16 +36,56 @@ func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayl | |||||
| 103 | ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold, | 36 | ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold, | ||
| 104 | StrictThreshold: resolved.WebProcessMemoryStrictThreshold, | 37 | StrictThreshold: resolved.WebProcessMemoryStrictThreshold, | ||
| 105 | }, | 38 | }, | ||
| 106 | - | Hardware: buildWebUIHardwarePayload(hw), | 39 | + | Hardware: buildSystemviewHardwarePayload(hw), |
| 40 | + | }, | |||
| 41 | + | } | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func buildSystemviewAppearancePayload(appearance AppearanceConfig) port.WebUIAppearanceConfig { | |||
| 45 | + | return port.WebUIAppearanceConfig{ | |||
| 46 | + | SansFont: appearance.SansFont, | |||
| 47 | + | SerifFont: appearance.SerifFont, | |||
| 48 | + | MonospaceFont: appearance.MonospaceFont, | |||
| 49 | + | DefaultFontSize: appearance.DefaultFontSize, | |||
| 50 | + | ColorScheme: appearance.ColorScheme, | |||
| 51 | + | LightPalette: port.ColorPalette{ | |||
| 52 | + | Background: appearance.LightPalette.Background, | |||
| 53 | + | Surface: appearance.LightPalette.Surface, | |||
| 54 | + | SurfaceVariant: appearance.LightPalette.SurfaceVariant, | |||
| 55 | + | Text: appearance.LightPalette.Text, | |||
| 56 | + | Muted: appearance.LightPalette.Muted, | |||
| 57 | + | Accent: appearance.LightPalette.Accent, | |||
| 58 | + | Border: appearance.LightPalette.Border, | |||
| 59 | + | }, | |||
| 60 | + | DarkPalette: port.ColorPalette{ | |||
| 61 | + | Background: appearance.DarkPalette.Background, | |||
| 62 | + | Surface: appearance.DarkPalette.Surface, | |||
| 63 | + | SurfaceVariant: appearance.DarkPalette.SurfaceVariant, | |||
| 64 | + | Text: appearance.DarkPalette.Text, | |||
| 65 | + | Muted: appearance.DarkPalette.Muted, | |||
| 66 | + | Accent: appearance.DarkPalette.Accent, | |||
| 67 | + | Border: appearance.DarkPalette.Border, | |||
| 107 | }, | 68 | }, | ||
| 108 | } | 69 | } | ||
| 109 | } | 70 | } | ||
| 110 | 71 | ||||
| 111 | - | func buildWebUIHardwarePayload(hw *port.HardwareInfo) WebUIConfigHardwarePayload { | 72 | + | func buildSystemviewSearchShortcutsPayload(shortcuts map[string]SearchShortcut) map[string]port.SearchShortcut { |
| 73 | + | if len(shortcuts) == 0 { | |||
| 74 | + | return nil | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | result := make(map[string]port.SearchShortcut, len(shortcuts)) | |||
| 78 | + | for key, shortcut := range shortcuts { | |||
| 79 | + | result[key] = port.SearchShortcut{URL: shortcut.URL, Description: shortcut.Description} | |||
| 80 | + | } | |||
| 81 | + | return result | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | func buildSystemviewHardwarePayload(hw *port.HardwareInfo) port.SystemviewHardwarePayload { | |||
| 112 | if hw == nil { | 85 | if hw == nil { | ||
| 113 | - | return WebUIConfigHardwarePayload{} | 86 | + | return port.SystemviewHardwarePayload{} |
| 114 | } | 87 | } | ||
| 115 | - | return WebUIConfigHardwarePayload{ | 88 | + | return port.SystemviewHardwarePayload{ |
| 116 | CPUCores: hw.CPUCores, | 89 | CPUCores: hw.CPUCores, | ||
| 117 | CPUThreads: hw.CPUThreads, | 90 | CPUThreads: hw.CPUThreads, | ||
| 118 | TotalRAMMB: hw.TotalRAMMB(), | 91 | TotalRAMMB: hw.TotalRAMMB(), | ||
M internal/infrastructure/webkit/engine_init.go +3 -0
| @@ -23,6 +23,8 @@ func NewEngine( | |||
| 23 | 23 | cfg *config.Config, | |
| 24 | 24 | opts port.EngineOptions, | |
| 25 | 25 | wkCfg WebKitEngineConfig, | |
| 26 | + | currentConfigPayload func() ([]byte, error), | |
| 27 | + | defaultConfigPayload func() ([]byte, error), | |
| 26 | 28 | themeManager *theme.Manager, | |
| 27 | 29 | colorResolver port.ColorSchemeResolver, | |
| 28 | 30 | logger zerolog.Logger, | |
| @@ -52,6 +54,7 @@ func NewEngine( | |||
| 52 | 54 | ||
| 53 | 55 | // --- Scheme handler --- | |
| 54 | 56 | schemeHandler := NewDumbSchemeHandler(ctx) | |
| 57 | + | schemeHandler.SetConfigPayloadBuilders(currentConfigPayload, defaultConfigPayload) | |
| 55 | 58 | schemeHandler.SetAssets(assets.WebUIAssets) | |
| 56 | 59 | schemeHandler.RegisterWithContext(wkCtx) | |
| 57 | 60 | ||
| @@ -23,6 +23,8 @@ func NewEngine( | |||||
| 23 | cfg *config.Config, | 23 | cfg *config.Config, | ||
| 24 | opts port.EngineOptions, | 24 | opts port.EngineOptions, | ||
| 25 | wkCfg WebKitEngineConfig, | 25 | wkCfg WebKitEngineConfig, | ||
| 26 | + | currentConfigPayload func() ([]byte, error), | |||
| 27 | + | defaultConfigPayload func() ([]byte, error), | |||
| 26 | themeManager *theme.Manager, | 28 | themeManager *theme.Manager, | ||
| 27 | colorResolver port.ColorSchemeResolver, | 29 | colorResolver port.ColorSchemeResolver, | ||
| 28 | logger zerolog.Logger, | 30 | logger zerolog.Logger, | ||
| @@ -52,6 +54,7 @@ func NewEngine( | |||||
| 52 | 54 | ||||
| 53 | // --- Scheme handler --- | 55 | // --- Scheme handler --- | ||
| 54 | schemeHandler := NewDumbSchemeHandler(ctx) | 56 | schemeHandler := NewDumbSchemeHandler(ctx) | ||
| 57 | + | schemeHandler.SetConfigPayloadBuilders(currentConfigPayload, defaultConfigPayload) | |||
| 55 | schemeHandler.SetAssets(assets.WebUIAssets) | 58 | schemeHandler.SetAssets(assets.WebUIAssets) | ||
| 56 | schemeHandler.RegisterWithContext(wkCtx) | 59 | schemeHandler.RegisterWithContext(wkCtx) | ||
| 57 | 60 | ||||
M internal/infrastructure/webkit/scheme_handler.go +35 -29
| @@ -3,7 +3,6 @@ package webkit | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "embed" | |
| 6 | - | "encoding/json" | |
| 7 | 6 | "fmt" | |
| 8 | 7 | "io/fs" | |
| 9 | 8 | "net/http" | |
| @@ -12,9 +11,6 @@ import ( | |||
| 12 | 11 | "strings" | |
| 13 | 12 | "sync" | |
| 14 | 13 | ||
| 15 | - | "github.com/bnema/dumber/internal/application/port" | |
| 16 | - | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 17 | - | "github.com/bnema/dumber/internal/infrastructure/env" | |
| 18 | 14 | "github.com/bnema/dumber/internal/infrastructure/webutil" | |
| 19 | 15 | "github.com/bnema/dumber/internal/logging" | |
| 20 | 16 | "github.com/bnema/puregotk-webkit/soup" | |
| @@ -64,13 +60,13 @@ func (f PageHandlerFunc) Handle(req *SchemeRequest) *SchemeResponse { | |||
| 64 | 60 | ||
| 65 | 61 | // DumbSchemeHandler handles dumb:// URI scheme requests. | |
| 66 | 62 | type DumbSchemeHandler struct { | |
| 67 | - | handlers map[string]PageHandler | |
| 68 | - | assets embed.FS | |
| 69 | - | assetDir string // subdirectory within embed.FS (e.g., "assets/webui") | |
| 70 | - | logger zerolog.Logger | |
| 71 | - | mu sync.RWMutex | |
| 72 | - | hwSurveyor *env.HardwareSurveyor | |
| 73 | - | ctx context.Context | |
| 63 | + | handlers map[string]PageHandler | |
| 64 | + | assets embed.FS | |
| 65 | + | assetDir string // subdirectory within embed.FS (e.g., "assets/webui") | |
| 66 | + | logger zerolog.Logger | |
| 67 | + | mu sync.RWMutex | |
| 68 | + | currentConfigPayload func() ([]byte, error) | |
| 69 | + | defaultConfigPayload func() ([]byte, error) | |
| 74 | 70 | } | |
| 75 | 71 | ||
| 76 | 72 | // NewDumbSchemeHandler creates a new handler for the dumb:// scheme. | |
| @@ -78,11 +74,9 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler { | |||
| 78 | 74 | log := logging.FromContext(ctx) | |
| 79 | 75 | ||
| 80 | 76 | h := &DumbSchemeHandler{ | |
| 81 | - | handlers: make(map[string]PageHandler), | |
| 82 | - | assetDir: "webui", | |
| 83 | - | logger: log.With().Str("component", "scheme-handler").Logger(), | |
| 84 | - | hwSurveyor: env.NewHardwareSurveyor(), | |
| 85 | - | ctx: ctx, | |
| 77 | + | handlers: make(map[string]PageHandler), | |
| 78 | + | assetDir: "webui", | |
| 79 | + | logger: log.With().Str("component", "scheme-handler").Logger(), | |
| 86 | 80 | } | |
| 87 | 81 | ||
| 88 | 82 | // Register default pages | |
| @@ -99,6 +93,15 @@ func (h *DumbSchemeHandler) SetAssets(assets embed.FS) { | |||
| 99 | 93 | h.logger.Debug().Msg("assets filesystem configured") | |
| 100 | 94 | } | |
| 101 | 95 | ||
| 96 | + | // SetConfigPayloadBuilders wires the config payload builders used by /api/config. | |
| 97 | + | func (h *DumbSchemeHandler) SetConfigPayloadBuilders(current, defaultPayload func() ([]byte, error)) { | |
| 98 | + | h.mu.Lock() | |
| 99 | + | defer h.mu.Unlock() | |
| 100 | + | h.currentConfigPayload = current | |
| 101 | + | h.defaultConfigPayload = defaultPayload | |
| 102 | + | h.logger.Debug().Msg("config payload builders configured") | |
| 103 | + | } | |
| 104 | + | ||
| 102 | 105 | // registerDefaults sets up default page handlers. | |
| 103 | 106 | func (h *DumbSchemeHandler) registerDefaults() { | |
| 104 | 107 | // Error page (static fallback) | |
| @@ -129,7 +132,10 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||
| 129 | 132 | return nil | |
| 130 | 133 | } | |
| 131 | 134 | ||
| 132 | - | return h.buildConfigResponse(config.Get()) | |
| 135 | + | h.mu.RLock() | |
| 136 | + | build := h.currentConfigPayload | |
| 137 | + | h.mu.RUnlock() | |
| 138 | + | return h.buildConfigResponse(build) | |
| 133 | 139 | })) | |
| 134 | 140 | ||
| 135 | 141 | // API: Get default config (used by Reset Defaults in dumb://config) | |
| @@ -138,7 +144,10 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||
| 138 | 144 | return nil | |
| 139 | 145 | } | |
| 140 | 146 | ||
| 141 | - | return h.buildConfigResponse(config.DefaultConfig()) | |
| 147 | + | h.mu.RLock() | |
| 148 | + | build := h.defaultConfigPayload | |
| 149 | + | h.mu.RUnlock() | |
| 150 | + | return h.buildConfigResponse(build) | |
| 142 | 151 | })) | |
| 143 | 152 | } | |
| 144 | 153 | ||
| @@ -161,19 +170,16 @@ func buildCrashPageHTML(originalURI string) string { | |||
| 161 | 170 | return webutil.BuildCrashPageHTML(originalURI) | |
| 162 | 171 | } | |
| 163 | 172 | ||
| 164 | - | func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse { | |
| 165 | - | // Get hardware info for display and profile resolution | |
| 166 | - | // Use background context since survey results are cached and we don't want | |
| 167 | - | // request context cancellation to affect this | |
| 168 | - | var hw *port.HardwareInfo | |
| 169 | - | if h.hwSurveyor != nil { | |
| 170 | - | hwInfo := h.hwSurveyor.Survey(context.Background()) | |
| 171 | - | hw = &hwInfo | |
| 173 | + | func (h *DumbSchemeHandler) buildConfigResponse(build func() ([]byte, error)) *SchemeResponse { | |
| 174 | + | if build == nil { | |
| 175 | + | return &SchemeResponse{ | |
| 176 | + | Data: []byte(`{"error": "config payload builder not configured"}`), | |
| 177 | + | ContentType: "application/json", | |
| 178 | + | StatusCode: http.StatusInternalServerError, | |
| 179 | + | } | |
| 172 | 180 | } | |
| 173 | 181 | ||
| 174 | - | resp := config.BuildWebUIConfigPayload(cfg, hw) | |
| 175 | - | ||
| 176 | - | data, err := json.Marshal(resp) | |
| 182 | + | data, err := build() | |
| 177 | 183 | if err != nil { | |
| 178 | 184 | return &SchemeResponse{ | |
| 179 | 185 | Data: []byte(fmt.Sprintf(`{"error": %q}`, err)), | |
| @@ -3,7 +3,6 @@ package webkit | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "embed" | 5 | "embed" | ||
| 6 | - | "encoding/json" | |||
| 7 | "fmt" | 6 | "fmt" | ||
| 8 | "io/fs" | 7 | "io/fs" | ||
| 9 | "net/http" | 8 | "net/http" | ||
| @@ -12,9 +11,6 @@ import ( | |||||
| 12 | "strings" | 11 | "strings" | ||
| 13 | "sync" | 12 | "sync" | ||
| 14 | 13 | ||||
| 15 | - | "github.com/bnema/dumber/internal/application/port" | |||
| 16 | - | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 17 | - | "github.com/bnema/dumber/internal/infrastructure/env" | |||
| 18 | "github.com/bnema/dumber/internal/infrastructure/webutil" | 14 | "github.com/bnema/dumber/internal/infrastructure/webutil" | ||
| 19 | "github.com/bnema/dumber/internal/logging" | 15 | "github.com/bnema/dumber/internal/logging" | ||
| 20 | "github.com/bnema/puregotk-webkit/soup" | 16 | "github.com/bnema/puregotk-webkit/soup" | ||
| @@ -64,13 +60,13 @@ func (f PageHandlerFunc) Handle(req *SchemeRequest) *SchemeResponse { | |||||
| 64 | 60 | ||||
| 65 | // DumbSchemeHandler handles dumb:// URI scheme requests. | 61 | // DumbSchemeHandler handles dumb:// URI scheme requests. | ||
| 66 | type DumbSchemeHandler struct { | 62 | type DumbSchemeHandler struct { | ||
| 67 | - | handlers map[string]PageHandler | 63 | + | handlers map[string]PageHandler |
| 68 | - | assets embed.FS | 64 | + | assets embed.FS |
| 69 | - | assetDir string // subdirectory within embed.FS (e.g., "assets/webui") | 65 | + | assetDir string // subdirectory within embed.FS (e.g., "assets/webui") |
| 70 | - | logger zerolog.Logger | 66 | + | logger zerolog.Logger |
| 71 | - | mu sync.RWMutex | 67 | + | mu sync.RWMutex |
| 72 | - | hwSurveyor *env.HardwareSurveyor | 68 | + | currentConfigPayload func() ([]byte, error) |
| 73 | - | ctx context.Context | 69 | + | defaultConfigPayload func() ([]byte, error) |
| 74 | } | 70 | } | ||
| 75 | 71 | ||||
| 76 | // NewDumbSchemeHandler creates a new handler for the dumb:// scheme. | 72 | // NewDumbSchemeHandler creates a new handler for the dumb:// scheme. | ||
| @@ -78,11 +74,9 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler { | |||||
| 78 | log := logging.FromContext(ctx) | 74 | log := logging.FromContext(ctx) | ||
| 79 | 75 | ||||
| 80 | h := &DumbSchemeHandler{ | 76 | h := &DumbSchemeHandler{ | ||
| 81 | - | handlers: make(map[string]PageHandler), | 77 | + | handlers: make(map[string]PageHandler), |
| 82 | - | assetDir: "webui", | 78 | + | assetDir: "webui", |
| 83 | - | logger: log.With().Str("component", "scheme-handler").Logger(), | 79 | + | logger: log.With().Str("component", "scheme-handler").Logger(), |
| 84 | - | hwSurveyor: env.NewHardwareSurveyor(), | |||
| 85 | - | ctx: ctx, | |||
| 86 | } | 80 | } | ||
| 87 | 81 | ||||
| 88 | // Register default pages | 82 | // Register default pages | ||
| @@ -99,6 +93,15 @@ func (h *DumbSchemeHandler) SetAssets(assets embed.FS) { | |||||
| 99 | h.logger.Debug().Msg("assets filesystem configured") | 93 | h.logger.Debug().Msg("assets filesystem configured") | ||
| 100 | } | 94 | } | ||
| 101 | 95 | ||||
| 96 | + | // SetConfigPayloadBuilders wires the config payload builders used by /api/config. | |||
| 97 | + | func (h *DumbSchemeHandler) SetConfigPayloadBuilders(current, defaultPayload func() ([]byte, error)) { | |||
| 98 | + | h.mu.Lock() | |||
| 99 | + | defer h.mu.Unlock() | |||
| 100 | + | h.currentConfigPayload = current | |||
| 101 | + | h.defaultConfigPayload = defaultPayload | |||
| 102 | + | h.logger.Debug().Msg("config payload builders configured") | |||
| 103 | + | } | |||
| 104 | + | ||||
| 102 | // registerDefaults sets up default page handlers. | 105 | // registerDefaults sets up default page handlers. | ||
| 103 | func (h *DumbSchemeHandler) registerDefaults() { | 106 | func (h *DumbSchemeHandler) registerDefaults() { | ||
| 104 | // Error page (static fallback) | 107 | // Error page (static fallback) | ||
| @@ -129,7 +132,10 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||||
| 129 | return nil | 132 | return nil | ||
| 130 | } | 133 | } | ||
| 131 | 134 | ||||
| 132 | - | return h.buildConfigResponse(config.Get()) | 135 | + | h.mu.RLock() |
| 136 | + | build := h.currentConfigPayload | |||
| 137 | + | h.mu.RUnlock() | |||
| 138 | + | return h.buildConfigResponse(build) | |||
| 133 | })) | 139 | })) | ||
| 134 | 140 | ||||
| 135 | // API: Get default config (used by Reset Defaults in dumb://config) | 141 | // API: Get default config (used by Reset Defaults in dumb://config) | ||
| @@ -138,7 +144,10 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||||
| 138 | return nil | 144 | return nil | ||
| 139 | } | 145 | } | ||
| 140 | 146 | ||||
| 141 | - | return h.buildConfigResponse(config.DefaultConfig()) | 147 | + | h.mu.RLock() |
| 148 | + | build := h.defaultConfigPayload | |||
| 149 | + | h.mu.RUnlock() | |||
| 150 | + | return h.buildConfigResponse(build) | |||
| 142 | })) | 151 | })) | ||
| 143 | } | 152 | } | ||
| 144 | 153 | ||||
| @@ -161,19 +170,16 @@ func buildCrashPageHTML(originalURI string) string { | |||||
| 161 | return webutil.BuildCrashPageHTML(originalURI) | 170 | return webutil.BuildCrashPageHTML(originalURI) | ||
| 162 | } | 171 | } | ||
| 163 | 172 | ||||
| 164 | - | func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse { | 173 | + | func (h *DumbSchemeHandler) buildConfigResponse(build func() ([]byte, error)) *SchemeResponse { |
| 165 | - | // Get hardware info for display and profile resolution | 174 | + | if build == nil { |
| 166 | - | // Use background context since survey results are cached and we don't want | 175 | + | return &SchemeResponse{ |
| 167 | - | // request context cancellation to affect this | 176 | + | Data: []byte(`{"error": "config payload builder not configured"}`), |
| 168 | - | var hw *port.HardwareInfo | 177 | + | ContentType: "application/json", |
| 169 | - | if h.hwSurveyor != nil { | 178 | + | StatusCode: http.StatusInternalServerError, |
| 170 | - | hwInfo := h.hwSurveyor.Survey(context.Background()) | 179 | + | } |
| 171 | - | hw = &hwInfo | |||
| 172 | } | 180 | } | ||
| 173 | 181 | ||||
| 174 | - | resp := config.BuildWebUIConfigPayload(cfg, hw) | 182 | + | data, err := build() |
| 175 | - | ||||
| 176 | - | data, err := json.Marshal(resp) | |||
| 177 | if err != nil { | 183 | if err != nil { | ||
| 178 | return &SchemeResponse{ | 184 | return &SchemeResponse{ | ||
| 179 | Data: []byte(fmt.Sprintf(`{"error": %q}`, err)), | 185 | Data: []byte(fmt.Sprintf(`{"error": %q}`, err)), | ||
M internal/infrastructure/webkit/scheme_handler_test.go +38 -0
| @@ -2,6 +2,7 @@ package webkit | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "net/http" | |
| 5 | 6 | "net/url" | |
| 6 | 7 | "testing" | |
| 7 | 8 | ||
| @@ -128,3 +129,40 @@ func TestHandleAsset_ConfigOpaqueFormServesConfigPage(t *testing.T) { | |||
| 128 | 129 | assert.Equal(t, "text/html; charset=utf-8", resp.ContentType) | |
| 129 | 130 | assert.Contains(t, string(resp.Data), "config") | |
| 130 | 131 | } | |
| 132 | + | ||
| 133 | + | func TestConfigHandlersUseInjectedPayloadBuilders(t *testing.T) { | |
| 134 | + | h := NewDumbSchemeHandler(context.Background()) | |
| 135 | + | h.SetConfigPayloadBuilders( | |
| 136 | + | func() ([]byte, error) { return []byte(`{"current":true}`), nil }, | |
| 137 | + | func() ([]byte, error) { return []byte(`{"default":true}`), nil }, | |
| 138 | + | ) | |
| 139 | + | ||
| 140 | + | h.mu.RLock() | |
| 141 | + | currentHandler := h.handlers["/api/config"] | |
| 142 | + | defaultHandler := h.handlers["/api/config/default"] | |
| 143 | + | h.mu.RUnlock() | |
| 144 | + | require.NotNil(t, currentHandler) | |
| 145 | + | require.NotNil(t, defaultHandler) | |
| 146 | + | ||
| 147 | + | currentResp := currentHandler.Handle(&SchemeRequest{ | |
| 148 | + | URI: "dumb://config/api/config", | |
| 149 | + | Path: "/api/config", | |
| 150 | + | Method: http.MethodGet, | |
| 151 | + | Scheme: "dumb", | |
| 152 | + | }) | |
| 153 | + | require.NotNil(t, currentResp) | |
| 154 | + | assert.Equal(t, http.StatusOK, currentResp.StatusCode) | |
| 155 | + | assert.Equal(t, "application/json", currentResp.ContentType) | |
| 156 | + | assert.Equal(t, []byte(`{"current":true}`), currentResp.Data) | |
| 157 | + | ||
| 158 | + | defaultResp := defaultHandler.Handle(&SchemeRequest{ | |
| 159 | + | URI: "dumb://config/api/config/default", | |
| 160 | + | Path: "/api/config/default", | |
| 161 | + | Method: http.MethodGet, | |
| 162 | + | Scheme: "dumb", | |
| 163 | + | }) | |
| 164 | + | require.NotNil(t, defaultResp) | |
| 165 | + | assert.Equal(t, http.StatusOK, defaultResp.StatusCode) | |
| 166 | + | assert.Equal(t, "application/json", defaultResp.ContentType) | |
| 167 | + | assert.Equal(t, []byte(`{"default":true}`), defaultResp.Data) | |
| 168 | + | } | |
| @@ -2,6 +2,7 @@ package webkit | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "net/http" | |||
| 5 | "net/url" | 6 | "net/url" | ||
| 6 | "testing" | 7 | "testing" | ||
| 7 | 8 | ||||
| @@ -128,3 +129,40 @@ func TestHandleAsset_ConfigOpaqueFormServesConfigPage(t *testing.T) { | |||||
| 128 | assert.Equal(t, "text/html; charset=utf-8", resp.ContentType) | 129 | assert.Equal(t, "text/html; charset=utf-8", resp.ContentType) | ||
| 129 | assert.Contains(t, string(resp.Data), "config") | 130 | assert.Contains(t, string(resp.Data), "config") | ||
| 130 | } | 131 | } | ||
| 132 | + | ||||
| 133 | + | func TestConfigHandlersUseInjectedPayloadBuilders(t *testing.T) { | |||
| 134 | + | h := NewDumbSchemeHandler(context.Background()) | |||
| 135 | + | h.SetConfigPayloadBuilders( | |||
| 136 | + | func() ([]byte, error) { return []byte(`{"current":true}`), nil }, | |||
| 137 | + | func() ([]byte, error) { return []byte(`{"default":true}`), nil }, | |||
| 138 | + | ) | |||
| 139 | + | ||||
| 140 | + | h.mu.RLock() | |||
| 141 | + | currentHandler := h.handlers["/api/config"] | |||
| 142 | + | defaultHandler := h.handlers["/api/config/default"] | |||
| 143 | + | h.mu.RUnlock() | |||
| 144 | + | require.NotNil(t, currentHandler) | |||
| 145 | + | require.NotNil(t, defaultHandler) | |||
| 146 | + | ||||
| 147 | + | currentResp := currentHandler.Handle(&SchemeRequest{ | |||
| 148 | + | URI: "dumb://config/api/config", | |||
| 149 | + | Path: "/api/config", | |||
| 150 | + | Method: http.MethodGet, | |||
| 151 | + | Scheme: "dumb", | |||
| 152 | + | }) | |||
| 153 | + | require.NotNil(t, currentResp) | |||
| 154 | + | assert.Equal(t, http.StatusOK, currentResp.StatusCode) | |||
| 155 | + | assert.Equal(t, "application/json", currentResp.ContentType) | |||
| 156 | + | assert.Equal(t, []byte(`{"current":true}`), currentResp.Data) | |||
| 157 | + | ||||
| 158 | + | defaultResp := defaultHandler.Handle(&SchemeRequest{ | |||
| 159 | + | URI: "dumb://config/api/config/default", | |||
| 160 | + | Path: "/api/config/default", | |||
| 161 | + | Method: http.MethodGet, | |||
| 162 | + | Scheme: "dumb", | |||
| 163 | + | }) | |||
| 164 | + | require.NotNil(t, defaultResp) | |||
| 165 | + | assert.Equal(t, http.StatusOK, defaultResp.StatusCode) | |||
| 166 | + | assert.Equal(t, "application/json", defaultResp.ContentType) | |||
| 167 | + | assert.Equal(t, []byte(`{"default":true}`), defaultResp.Data) | |||
| 168 | + | } | |||