chore: fix lint issues across phase streams
- commit
- 00ab191e33cff1c712aee7411f50e1527118fe7d
- author
- brice · · 2026-02-07 05:17 UTC
- parent
- f270317
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +180 -164
M internal/application/usecase/navigate.go +14 -22
| @@ -147,10 +147,10 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||
| 147 | 147 | return | |
| 148 | 148 | } | |
| 149 | 149 | ||
| 150 | - | url := canonicalizeURLForHistory(rawURL, historyCanonicalizationOptions{ | |
| 150 | + | canonicalURL := canonicalizeURLForHistory(rawURL, historyCanonicalizationOptions{ | |
| 151 | 151 | StripTrackingParams: stripTrackingParamsForHistoryDedup, | |
| 152 | 152 | }) | |
| 153 | - | if url == "" { | |
| 153 | + | if canonicalURL == "" { | |
| 154 | 154 | return | |
| 155 | 155 | } | |
| 156 | 156 | ||
| @@ -172,7 +172,7 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||
| 172 | 172 | } | |
| 173 | 173 | ||
| 174 | 174 | // Per-pane short-window deduplication by canonical URL. | |
| 175 | - | if state.lastCanonicalURL == url && now.Sub(state.lastRecordedAt) < historyDeduplicationWindow { | |
| 175 | + | if state.lastCanonicalURL == canonicalURL && now.Sub(state.lastRecordedAt) < historyDeduplicationWindow { | |
| 176 | 176 | state.lastRawURL = rawURL | |
| 177 | 177 | uc.recentVisits[paneID] = state | |
| 178 | 178 | uc.recentMu.Unlock() | |
| @@ -180,17 +180,17 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||
| 180 | 180 | } | |
| 181 | 181 | ||
| 182 | 182 | state.lastRawURL = rawURL | |
| 183 | - | state.lastCanonicalURL = url | |
| 183 | + | state.lastCanonicalURL = canonicalURL | |
| 184 | 184 | state.lastRecordedAt = now | |
| 185 | 185 | uc.recentVisits[paneID] = state | |
| 186 | 186 | uc.recentMu.Unlock() | |
| 187 | 187 | ||
| 188 | 188 | // Non-blocking send to async queue | |
| 189 | 189 | select { | |
| 190 | - | case uc.historyQueue <- historyRecord{url: url, visits: 1}: | |
| 190 | + | case uc.historyQueue <- historyRecord{url: canonicalURL, visits: 1}: | |
| 191 | 191 | default: | |
| 192 | 192 | // Queue full - log warning but don't block navigation | |
| 193 | - | log.Warn().Str("url", logging.TruncateURL(url, logURLMaxLen)).Msg("history queue full, dropping record") | |
| 193 | + | log.Warn().Str("url", logging.TruncateURL(canonicalURL, logURLMaxLen)).Msg("history queue full, dropping record") | |
| 194 | 194 | } | |
| 195 | 195 | } | |
| 196 | 196 | ||
| @@ -258,7 +258,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||
| 258 | 258 | } | |
| 259 | 259 | ||
| 260 | 260 | if existing != nil { | |
| 261 | - | for i := 0; i < max(1, record.visits); i++ { | |
| 261 | + | for i := 0; i < maxInt(1, record.visits); i++ { | |
| 262 | 262 | if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil { | |
| 263 | 263 | log.Warn().Err(err).Str("url", record.url).Msg("failed to increment visit count") | |
| 264 | 264 | return | |
| @@ -267,7 +267,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||
| 267 | 267 | } else { | |
| 268 | 268 | // Create new entry | |
| 269 | 269 | entry := entity.NewHistoryEntry(record.url, "") | |
| 270 | - | entry.VisitCount = int64(max(1, record.visits)) | |
| 270 | + | entry.VisitCount = int64(maxInt(1, record.visits)) | |
| 271 | 271 | if err := uc.historyRepo.Save(ctx, entry); err != nil { | |
| 272 | 272 | log.Warn().Err(err).Str("url", record.url).Msg("failed to save history") | |
| 273 | 273 | } | |
| @@ -275,16 +275,16 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||
| 275 | 275 | } | |
| 276 | 276 | ||
| 277 | 277 | // UpdateHistoryTitle updates the title of a history entry after page load. | |
| 278 | - | func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title string) error { | |
| 278 | + | func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, historyURL, title string) error { | |
| 279 | 279 | log := logging.FromContext(ctx) | |
| 280 | 280 | ||
| 281 | 281 | // Canonicalize URL the same way history records are persisted. | |
| 282 | - | url = canonicalizeURLForHistory(url, historyCanonicalizationOptions{ | |
| 282 | + | historyURL = canonicalizeURLForHistory(historyURL, historyCanonicalizationOptions{ | |
| 283 | 283 | StripTrackingParams: stripTrackingParamsForHistoryDedup, | |
| 284 | 284 | }) | |
| 285 | - | log.Debug().Str("url", logging.TruncateURL(url, logURLMaxLen)).Str("title", title).Msg("updating history title") | |
| 285 | + | log.Debug().Str("url", logging.TruncateURL(historyURL, logURLMaxLen)).Str("title", title).Msg("updating history title") | |
| 286 | 286 | ||
| 287 | - | entry, err := uc.historyRepo.FindByURL(ctx, url) | |
| 287 | + | entry, err := uc.historyRepo.FindByURL(ctx, historyURL) | |
| 288 | 288 | if err != nil { | |
| 289 | 289 | return fmt.Errorf("failed to find history entry: %w", err) | |
| 290 | 290 | } | |
| @@ -293,7 +293,7 @@ func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title st | |||
| 293 | 293 | // Entry doesn't exist - don't create it here | |
| 294 | 294 | // Initial navigation should have already created the entry | |
| 295 | 295 | // This can happen if URL changed during page load (SPA, redirect) | |
| 296 | - | log.Debug().Str("url", url).Msg("no history entry found for URL, skipping title update") | |
| 296 | + | log.Debug().Str("url", historyURL).Msg("no history entry found for URL, skipping title update") | |
| 297 | 297 | return nil | |
| 298 | 298 | } | |
| 299 | 299 | ||
| @@ -324,14 +324,6 @@ func (uc *NavigateUseCase) Stop(ctx context.Context, webview port.WebView) error | |||
| 324 | 324 | return webview.Stop(ctx) | |
| 325 | 325 | } | |
| 326 | 326 | ||
| 327 | - | // normalizeURLForHistory normalizes a URL for history storage. | |
| 328 | - | // Strips trailing slash to avoid duplicates like github.com vs github.com/ | |
| 329 | - | func normalizeURLForHistory(raw string) string { | |
| 330 | - | return canonicalizeURLForHistory(raw, historyCanonicalizationOptions{ | |
| 331 | - | StripTrackingParams: false, | |
| 332 | - | }) | |
| 333 | - | } | |
| 334 | - | ||
| 335 | 327 | func canonicalizeURLForHistory(raw string, opts historyCanonicalizationOptions) string { | |
| 336 | 328 | raw = strings.TrimSpace(raw) | |
| 337 | 329 | if raw == "" { | |
| @@ -412,7 +404,7 @@ func isHashOnlyTransition(previous, current string) bool { | |||
| 412 | 404 | return prevParsed.Fragment != currParsed.Fragment | |
| 413 | 405 | } | |
| 414 | 406 | ||
| 415 | - | func max(a, b int) int { | |
| 407 | + | func maxInt(a, b int) int { | |
| 416 | 408 | if a > b { | |
| 417 | 409 | return a | |
| 418 | 410 | } | |
| @@ -147,10 +147,10 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||||
| 147 | return | 147 | return | ||
| 148 | } | 148 | } | ||
| 149 | 149 | ||||
| 150 | - | url := canonicalizeURLForHistory(rawURL, historyCanonicalizationOptions{ | 150 | + | canonicalURL := canonicalizeURLForHistory(rawURL, historyCanonicalizationOptions{ |
| 151 | StripTrackingParams: stripTrackingParamsForHistoryDedup, | 151 | StripTrackingParams: stripTrackingParamsForHistoryDedup, | ||
| 152 | }) | 152 | }) | ||
| 153 | - | if url == "" { | 153 | + | if canonicalURL == "" { |
| 154 | return | 154 | return | ||
| 155 | } | 155 | } | ||
| 156 | 156 | ||||
| @@ -172,7 +172,7 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||||
| 172 | } | 172 | } | ||
| 173 | 173 | ||||
| 174 | // Per-pane short-window deduplication by canonical URL. | 174 | // Per-pane short-window deduplication by canonical URL. | ||
| 175 | - | if state.lastCanonicalURL == url && now.Sub(state.lastRecordedAt) < historyDeduplicationWindow { | 175 | + | if state.lastCanonicalURL == canonicalURL && now.Sub(state.lastRecordedAt) < historyDeduplicationWindow { |
| 176 | state.lastRawURL = rawURL | 176 | state.lastRawURL = rawURL | ||
| 177 | uc.recentVisits[paneID] = state | 177 | uc.recentVisits[paneID] = state | ||
| 178 | uc.recentMu.Unlock() | 178 | uc.recentMu.Unlock() | ||
| @@ -180,17 +180,17 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||||
| 180 | } | 180 | } | ||
| 181 | 181 | ||||
| 182 | state.lastRawURL = rawURL | 182 | state.lastRawURL = rawURL | ||
| 183 | - | state.lastCanonicalURL = url | 183 | + | state.lastCanonicalURL = canonicalURL |
| 184 | state.lastRecordedAt = now | 184 | state.lastRecordedAt = now | ||
| 185 | uc.recentVisits[paneID] = state | 185 | uc.recentVisits[paneID] = state | ||
| 186 | uc.recentMu.Unlock() | 186 | uc.recentMu.Unlock() | ||
| 187 | 187 | ||||
| 188 | // Non-blocking send to async queue | 188 | // Non-blocking send to async queue | ||
| 189 | select { | 189 | select { | ||
| 190 | - | case uc.historyQueue <- historyRecord{url: url, visits: 1}: | 190 | + | case uc.historyQueue <- historyRecord{url: canonicalURL, visits: 1}: |
| 191 | default: | 191 | default: | ||
| 192 | // Queue full - log warning but don't block navigation | 192 | // Queue full - log warning but don't block navigation | ||
| 193 | - | log.Warn().Str("url", logging.TruncateURL(url, logURLMaxLen)).Msg("history queue full, dropping record") | 193 | + | log.Warn().Str("url", logging.TruncateURL(canonicalURL, logURLMaxLen)).Msg("history queue full, dropping record") |
| 194 | } | 194 | } | ||
| 195 | } | 195 | } | ||
| 196 | 196 | ||||
| @@ -258,7 +258,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||||
| 258 | } | 258 | } | ||
| 259 | 259 | ||||
| 260 | if existing != nil { | 260 | if existing != nil { | ||
| 261 | - | for i := 0; i < max(1, record.visits); i++ { | 261 | + | for i := 0; i < maxInt(1, record.visits); i++ { |
| 262 | if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil { | 262 | if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil { | ||
| 263 | log.Warn().Err(err).Str("url", record.url).Msg("failed to increment visit count") | 263 | log.Warn().Err(err).Str("url", record.url).Msg("failed to increment visit count") | ||
| 264 | return | 264 | return | ||
| @@ -267,7 +267,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||||
| 267 | } else { | 267 | } else { | ||
| 268 | // Create new entry | 268 | // Create new entry | ||
| 269 | entry := entity.NewHistoryEntry(record.url, "") | 269 | entry := entity.NewHistoryEntry(record.url, "") | ||
| 270 | - | entry.VisitCount = int64(max(1, record.visits)) | 270 | + | entry.VisitCount = int64(maxInt(1, record.visits)) |
| 271 | if err := uc.historyRepo.Save(ctx, entry); err != nil { | 271 | if err := uc.historyRepo.Save(ctx, entry); err != nil { | ||
| 272 | log.Warn().Err(err).Str("url", record.url).Msg("failed to save history") | 272 | log.Warn().Err(err).Str("url", record.url).Msg("failed to save history") | ||
| 273 | } | 273 | } | ||
| @@ -275,16 +275,16 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||||
| 275 | } | 275 | } | ||
| 276 | 276 | ||||
| 277 | // UpdateHistoryTitle updates the title of a history entry after page load. | 277 | // UpdateHistoryTitle updates the title of a history entry after page load. | ||
| 278 | - | func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title string) error { | 278 | + | func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, historyURL, title string) error { |
| 279 | log := logging.FromContext(ctx) | 279 | log := logging.FromContext(ctx) | ||
| 280 | 280 | ||||
| 281 | // Canonicalize URL the same way history records are persisted. | 281 | // Canonicalize URL the same way history records are persisted. | ||
| 282 | - | url = canonicalizeURLForHistory(url, historyCanonicalizationOptions{ | 282 | + | historyURL = canonicalizeURLForHistory(historyURL, historyCanonicalizationOptions{ |
| 283 | StripTrackingParams: stripTrackingParamsForHistoryDedup, | 283 | StripTrackingParams: stripTrackingParamsForHistoryDedup, | ||
| 284 | }) | 284 | }) | ||
| 285 | - | log.Debug().Str("url", logging.TruncateURL(url, logURLMaxLen)).Str("title", title).Msg("updating history title") | 285 | + | log.Debug().Str("url", logging.TruncateURL(historyURL, logURLMaxLen)).Str("title", title).Msg("updating history title") |
| 286 | 286 | ||||
| 287 | - | entry, err := uc.historyRepo.FindByURL(ctx, url) | 287 | + | entry, err := uc.historyRepo.FindByURL(ctx, historyURL) |
| 288 | if err != nil { | 288 | if err != nil { | ||
| 289 | return fmt.Errorf("failed to find history entry: %w", err) | 289 | return fmt.Errorf("failed to find history entry: %w", err) | ||
| 290 | } | 290 | } | ||
| @@ -293,7 +293,7 @@ func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title st | |||||
| 293 | // Entry doesn't exist - don't create it here | 293 | // Entry doesn't exist - don't create it here | ||
| 294 | // Initial navigation should have already created the entry | 294 | // Initial navigation should have already created the entry | ||
| 295 | // This can happen if URL changed during page load (SPA, redirect) | 295 | // This can happen if URL changed during page load (SPA, redirect) | ||
| 296 | - | log.Debug().Str("url", url).Msg("no history entry found for URL, skipping title update") | 296 | + | log.Debug().Str("url", historyURL).Msg("no history entry found for URL, skipping title update") |
| 297 | return nil | 297 | return nil | ||
| 298 | } | 298 | } | ||
| 299 | 299 | ||||
| @@ -324,14 +324,6 @@ func (uc *NavigateUseCase) Stop(ctx context.Context, webview port.WebView) error | |||||
| 324 | return webview.Stop(ctx) | 324 | return webview.Stop(ctx) | ||
| 325 | } | 325 | } | ||
| 326 | 326 | ||||
| 327 | - | // normalizeURLForHistory normalizes a URL for history storage. | |||
| 328 | - | // Strips trailing slash to avoid duplicates like github.com vs github.com/ | |||
| 329 | - | func normalizeURLForHistory(raw string) string { | |||
| 330 | - | return canonicalizeURLForHistory(raw, historyCanonicalizationOptions{ | |||
| 331 | - | StripTrackingParams: false, | |||
| 332 | - | }) | |||
| 333 | - | } | |||
| 334 | - | ||||
| 335 | func canonicalizeURLForHistory(raw string, opts historyCanonicalizationOptions) string { | 327 | func canonicalizeURLForHistory(raw string, opts historyCanonicalizationOptions) string { | ||
| 336 | raw = strings.TrimSpace(raw) | 328 | raw = strings.TrimSpace(raw) | ||
| 337 | if raw == "" { | 329 | if raw == "" { | ||
| @@ -412,7 +404,7 @@ func isHashOnlyTransition(previous, current string) bool { | |||||
| 412 | return prevParsed.Fragment != currParsed.Fragment | 404 | return prevParsed.Fragment != currParsed.Fragment | ||
| 413 | } | 405 | } | ||
| 414 | 406 | ||||
| 415 | - | func max(a, b int) int { | 407 | + | func maxInt(a, b int) int { |
| 416 | if a > b { | 408 | if a > b { | ||
| 417 | return a | 409 | return a | ||
| 418 | } | 410 | } | ||
M internal/bootstrap/webkit_stack_test.go +4 -4
| @@ -32,13 +32,13 @@ func TestBuildWebKitContextOptions_MapsPrivacyAndMemory(t *testing.T) { | |||
| 32 | 32 | ||
| 33 | 33 | require.NotNil(t, opts.WebProcessMemory) | |
| 34 | 34 | assert.Equal(t, 1024, opts.WebProcessMemory.MemoryLimitMB) | |
| 35 | - | assert.Equal(t, 11.0, opts.WebProcessMemory.PollIntervalSec) | |
| 36 | - | assert.Equal(t, 0.25, opts.WebProcessMemory.ConservativeThreshold) | |
| 37 | - | assert.Equal(t, 0.5, opts.WebProcessMemory.StrictThreshold) | |
| 35 | + | assert.InDelta(t, 11.0, opts.WebProcessMemory.PollIntervalSec, 1e-9) | |
| 36 | + | assert.InDelta(t, 0.25, opts.WebProcessMemory.ConservativeThreshold, 1e-9) | |
| 37 | + | assert.InDelta(t, 0.5, opts.WebProcessMemory.StrictThreshold, 1e-9) | |
| 38 | 38 | ||
| 39 | 39 | require.NotNil(t, opts.NetworkProcessMemory) | |
| 40 | 40 | assert.Equal(t, 512, opts.NetworkProcessMemory.MemoryLimitMB) | |
| 41 | - | assert.Equal(t, 13.0, opts.NetworkProcessMemory.PollIntervalSec) | |
| 41 | + | assert.InDelta(t, 13.0, opts.NetworkProcessMemory.PollIntervalSec, 1e-9) | |
| 42 | 42 | } | |
| 43 | 43 | ||
| 44 | 44 | func TestBuildWebKitContextOptions_DefaultPrivacyWhenConfigNil(t *testing.T) { | |
| @@ -32,13 +32,13 @@ func TestBuildWebKitContextOptions_MapsPrivacyAndMemory(t *testing.T) { | |||||
| 32 | 32 | ||||
| 33 | require.NotNil(t, opts.WebProcessMemory) | 33 | require.NotNil(t, opts.WebProcessMemory) | ||
| 34 | assert.Equal(t, 1024, opts.WebProcessMemory.MemoryLimitMB) | 34 | assert.Equal(t, 1024, opts.WebProcessMemory.MemoryLimitMB) | ||
| 35 | - | assert.Equal(t, 11.0, opts.WebProcessMemory.PollIntervalSec) | 35 | + | assert.InDelta(t, 11.0, opts.WebProcessMemory.PollIntervalSec, 1e-9) |
| 36 | - | assert.Equal(t, 0.25, opts.WebProcessMemory.ConservativeThreshold) | 36 | + | assert.InDelta(t, 0.25, opts.WebProcessMemory.ConservativeThreshold, 1e-9) |
| 37 | - | assert.Equal(t, 0.5, opts.WebProcessMemory.StrictThreshold) | 37 | + | assert.InDelta(t, 0.5, opts.WebProcessMemory.StrictThreshold, 1e-9) |
| 38 | 38 | ||||
| 39 | require.NotNil(t, opts.NetworkProcessMemory) | 39 | require.NotNil(t, opts.NetworkProcessMemory) | ||
| 40 | assert.Equal(t, 512, opts.NetworkProcessMemory.MemoryLimitMB) | 40 | assert.Equal(t, 512, opts.NetworkProcessMemory.MemoryLimitMB) | ||
| 41 | - | assert.Equal(t, 13.0, opts.NetworkProcessMemory.PollIntervalSec) | 41 | + | assert.InDelta(t, 13.0, opts.NetworkProcessMemory.PollIntervalSec, 1e-9) |
| 42 | } | 42 | } | ||
| 43 | 43 | ||||
| 44 | func TestBuildWebKitContextOptions_DefaultPrivacyWhenConfigNil(t *testing.T) { | 44 | func TestBuildWebKitContextOptions_DefaultPrivacyWhenConfigNil(t *testing.T) { | ||
M internal/infrastructure/config/loader.go +16 -3
| @@ -177,6 +177,16 @@ func ensureDatabasePath(config *Config) error { | |||
| 177 | 177 | } | |
| 178 | 178 | ||
| 179 | 179 | func normalizeConfig(config *Config) { | |
| 180 | + | normalizeRendering(config) | |
| 181 | + | normalizePrivacy(config) | |
| 182 | + | normalizeAppearance(config) | |
| 183 | + | normalizeMedia(config) | |
| 184 | + | ||
| 185 | + | config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix) | |
| 186 | + | normalizePerformanceProfile(config) | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | func normalizeRendering(config *Config) { | |
| 180 | 190 | switch strings.ToLower(string(config.Rendering.Mode)) { | |
| 181 | 191 | case "", string(RenderingModeAuto): | |
| 182 | 192 | config.Rendering.Mode = RenderingModeAuto | |
| @@ -200,7 +210,9 @@ func normalizeConfig(config *Config) { | |||
| 200 | 210 | default: | |
| 201 | 211 | config.Rendering.GSKRenderer = GSKRendererAuto | |
| 202 | 212 | } | |
| 213 | + | } | |
| 203 | 214 | ||
| 215 | + | func normalizePrivacy(config *Config) { | |
| 204 | 216 | switch strings.ToLower(string(config.Privacy.CookiePolicy)) { | |
| 205 | 217 | case "", string(CookiePolicyNoThirdParty): | |
| 206 | 218 | config.Privacy.CookiePolicy = CookiePolicyNoThirdParty | |
| @@ -211,7 +223,9 @@ func normalizeConfig(config *Config) { | |||
| 211 | 223 | default: | |
| 212 | 224 | config.Privacy.CookiePolicy = CookiePolicyNoThirdParty | |
| 213 | 225 | } | |
| 226 | + | } | |
| 214 | 227 | ||
| 228 | + | func normalizeAppearance(config *Config) { | |
| 215 | 229 | switch config.Appearance.ColorScheme { | |
| 216 | 230 | case ThemePreferDark, ThemePreferLight, ThemeDefault: | |
| 217 | 231 | case "": | |
| @@ -219,7 +233,9 @@ func normalizeConfig(config *Config) { | |||
| 219 | 233 | default: | |
| 220 | 234 | config.Appearance.ColorScheme = ThemeDefault | |
| 221 | 235 | } | |
| 236 | + | } | |
| 222 | 237 | ||
| 238 | + | func normalizeMedia(config *Config) { | |
| 223 | 239 | switch strings.ToLower(string(config.Media.HardwareDecodingMode)) { | |
| 224 | 240 | case "", string(HardwareDecodingAuto): | |
| 225 | 241 | config.Media.HardwareDecodingMode = HardwareDecodingAuto | |
| @@ -243,9 +259,6 @@ func normalizeConfig(config *Config) { | |||
| 243 | 259 | default: | |
| 244 | 260 | config.Media.GLRenderingMode = GLRenderingModeAuto | |
| 245 | 261 | } | |
| 246 | - | ||
| 247 | - | config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix) | |
| 248 | - | normalizePerformanceProfile(config) | |
| 249 | 262 | } | |
| 250 | 263 | ||
| 251 | 264 | func normalizePerformanceProfile(config *Config) { | |
| @@ -177,6 +177,16 @@ func ensureDatabasePath(config *Config) error { | |||||
| 177 | } | 177 | } | ||
| 178 | 178 | ||||
| 179 | func normalizeConfig(config *Config) { | 179 | func normalizeConfig(config *Config) { | ||
| 180 | + | normalizeRendering(config) | |||
| 181 | + | normalizePrivacy(config) | |||
| 182 | + | normalizeAppearance(config) | |||
| 183 | + | normalizeMedia(config) | |||
| 184 | + | ||||
| 185 | + | config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix) | |||
| 186 | + | normalizePerformanceProfile(config) | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | func normalizeRendering(config *Config) { | |||
| 180 | switch strings.ToLower(string(config.Rendering.Mode)) { | 190 | switch strings.ToLower(string(config.Rendering.Mode)) { | ||
| 181 | case "", string(RenderingModeAuto): | 191 | case "", string(RenderingModeAuto): | ||
| 182 | config.Rendering.Mode = RenderingModeAuto | 192 | config.Rendering.Mode = RenderingModeAuto | ||
| @@ -200,7 +210,9 @@ func normalizeConfig(config *Config) { | |||||
| 200 | default: | 210 | default: | ||
| 201 | config.Rendering.GSKRenderer = GSKRendererAuto | 211 | config.Rendering.GSKRenderer = GSKRendererAuto | ||
| 202 | } | 212 | } | ||
| 213 | + | } | |||
| 203 | 214 | ||||
| 215 | + | func normalizePrivacy(config *Config) { | |||
| 204 | switch strings.ToLower(string(config.Privacy.CookiePolicy)) { | 216 | switch strings.ToLower(string(config.Privacy.CookiePolicy)) { | ||
| 205 | case "", string(CookiePolicyNoThirdParty): | 217 | case "", string(CookiePolicyNoThirdParty): | ||
| 206 | config.Privacy.CookiePolicy = CookiePolicyNoThirdParty | 218 | config.Privacy.CookiePolicy = CookiePolicyNoThirdParty | ||
| @@ -211,7 +223,9 @@ func normalizeConfig(config *Config) { | |||||
| 211 | default: | 223 | default: | ||
| 212 | config.Privacy.CookiePolicy = CookiePolicyNoThirdParty | 224 | config.Privacy.CookiePolicy = CookiePolicyNoThirdParty | ||
| 213 | } | 225 | } | ||
| 226 | + | } | |||
| 214 | 227 | ||||
| 228 | + | func normalizeAppearance(config *Config) { | |||
| 215 | switch config.Appearance.ColorScheme { | 229 | switch config.Appearance.ColorScheme { | ||
| 216 | case ThemePreferDark, ThemePreferLight, ThemeDefault: | 230 | case ThemePreferDark, ThemePreferLight, ThemeDefault: | ||
| 217 | case "": | 231 | case "": | ||
| @@ -219,7 +233,9 @@ func normalizeConfig(config *Config) { | |||||
| 219 | default: | 233 | default: | ||
| 220 | config.Appearance.ColorScheme = ThemeDefault | 234 | config.Appearance.ColorScheme = ThemeDefault | ||
| 221 | } | 235 | } | ||
| 236 | + | } | |||
| 222 | 237 | ||||
| 238 | + | func normalizeMedia(config *Config) { | |||
| 223 | switch strings.ToLower(string(config.Media.HardwareDecodingMode)) { | 239 | switch strings.ToLower(string(config.Media.HardwareDecodingMode)) { | ||
| 224 | case "", string(HardwareDecodingAuto): | 240 | case "", string(HardwareDecodingAuto): | ||
| 225 | config.Media.HardwareDecodingMode = HardwareDecodingAuto | 241 | config.Media.HardwareDecodingMode = HardwareDecodingAuto | ||
| @@ -243,9 +259,6 @@ func normalizeConfig(config *Config) { | |||||
| 243 | default: | 259 | default: | ||
| 244 | config.Media.GLRenderingMode = GLRenderingModeAuto | 260 | config.Media.GLRenderingMode = GLRenderingModeAuto | ||
| 245 | } | 261 | } | ||
| 246 | - | ||||
| 247 | - | config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix) | |||
| 248 | - | normalizePerformanceProfile(config) | |||
| 249 | } | 262 | } | ||
| 250 | 263 | ||||
| 251 | func normalizePerformanceProfile(config *Config) { | 264 | func normalizePerformanceProfile(config *Config) { | ||
M internal/infrastructure/filtering/manager.go +112 -101
| @@ -141,115 +141,125 @@ func (m *Manager) LoadAsync(ctx context.Context) { | |||
| 141 | 141 | return | |
| 142 | 142 | } | |
| 143 | 143 | ||
| 144 | - | go func() { | |
| 145 | - | log := logging.FromContext(ctx).With(). | |
| 146 | - | Str("component", "filter-manager"). | |
| 147 | - | Logger() | |
| 148 | - | ||
| 149 | - | // If already loaded, skip | |
| 150 | - | m.filterMu.RLock() | |
| 151 | - | if m.filter != nil { | |
| 152 | - | m.filterMu.RUnlock() | |
| 153 | - | log.Debug().Msg("filters already loaded, skipping async load") | |
| 154 | - | return | |
| 155 | - | } | |
| 156 | - | m.filterMu.RUnlock() | |
| 157 | - | ||
| 158 | - | // Try to load from cache first (fast path) | |
| 159 | - | m.setStatus(FilterStatus{State: StateLoading, Message: "Loading filters..."}) | |
| 160 | - | if m.store.HasCompiledFilter(ctx, FilterIdentifier) { | |
| 161 | - | log.Debug().Msg("found compiled filter, loading from cache") | |
| 162 | - | filter, err := m.store.Load(ctx, FilterIdentifier) | |
| 163 | - | if err == nil && filter != nil { | |
| 164 | - | m.filterMu.Lock() | |
| 165 | - | m.filter = filter | |
| 166 | - | m.filterMu.Unlock() | |
| 167 | - | ||
| 168 | - | version := m.getCachedVersion() | |
| 169 | - | m.setStatus(FilterStatus{ | |
| 170 | - | State: StateActive, | |
| 171 | - | Message: "Filters active", | |
| 172 | - | Version: version, | |
| 173 | - | }) | |
| 174 | - | log.Info().Str("version", version).Msg("filters loaded from cache") | |
| 175 | - | ||
| 176 | - | m.checkStaleCacheAndUpdate(ctx) | |
| 177 | - | return | |
| 178 | - | } | |
| 179 | - | log.Warn().Err(err).Msg("failed to load cached filter, will download") | |
| 180 | - | } | |
| 144 | + | go m.loadAsyncWorker(ctx) | |
| 145 | + | } | |
| 181 | 146 | ||
| 182 | - | // Download filters | |
| 183 | - | m.setStatus(FilterStatus{State: StateLoading, Message: "Downloading filters..."}) | |
| 184 | - | paths, err := m.downloader.DownloadFilters(ctx, func(p DownloadProgress) { | |
| 185 | - | msg := fmt.Sprintf("Downloading filters (%d/%d)...", p.Current, p.Total) | |
| 186 | - | m.setStatus(FilterStatus{State: StateLoading, Message: msg}) | |
| 187 | - | }) | |
| 188 | - | if err != nil { | |
| 189 | - | log.Error().Err(err).Msg("failed to download filters") | |
| 190 | - | m.setStatus(FilterStatus{State: StateError, Message: "Download failed"}) | |
| 191 | - | return | |
| 192 | - | } | |
| 147 | + | func (m *Manager) loadAsyncWorker(ctx context.Context) { | |
| 148 | + | log := logging.FromContext(ctx).With(). | |
| 149 | + | Str("component", "filter-manager"). | |
| 150 | + | Logger() | |
| 193 | 151 | ||
| 194 | - | if err := m.validateDownloadedFiles(ctx, paths); err != nil { | |
| 195 | - | log.Error().Err(err).Msg("downloaded filter validation failed") | |
| 196 | - | m.setStatus(FilterStatus{State: StateError, Message: "Invalid downloaded filters"}) | |
| 197 | - | return | |
| 198 | - | } | |
| 152 | + | if m.hasActiveFilter() { | |
| 153 | + | log.Debug().Msg("filters already loaded, skipping async load") | |
| 154 | + | return | |
| 155 | + | } | |
| 199 | 156 | ||
| 200 | - | m.setStatus(FilterStatus{State: StateLoading, Message: "Compiling filters..."}) | |
| 157 | + | if m.loadFromCache(ctx) { | |
| 158 | + | m.checkStaleCacheAndUpdate(ctx) | |
| 159 | + | return | |
| 160 | + | } | |
| 201 | 161 | ||
| 202 | - | // Merge JSON files into one for compilation | |
| 203 | - | mergedPath, err := m.mergeJSONFiles(ctx, paths) | |
| 204 | - | if err != nil { | |
| 205 | - | log.Error().Err(err).Msg("failed to merge filter files") | |
| 206 | - | m.setStatus(FilterStatus{State: StateError, Message: "Filter merge failed"}) | |
| 207 | - | return | |
| 208 | - | } | |
| 162 | + | m.downloadCompileAndActivate(ctx) | |
| 163 | + | } | |
| 209 | 164 | ||
| 210 | - | // Compile the merged filter | |
| 211 | - | filter, err := m.store.Compile(ctx, FilterIdentifier, mergedPath) | |
| 212 | - | if err != nil { | |
| 213 | - | log.Error().Err(err).Msg("failed to compile filters") | |
| 214 | - | if m.hasActiveFilter() { | |
| 215 | - | version := m.getCachedVersion() | |
| 216 | - | m.setStatus(FilterStatus{ | |
| 217 | - | State: StateActive, | |
| 218 | - | Message: "Filters active (update skipped)", | |
| 219 | - | Version: version, | |
| 220 | - | }) | |
| 221 | - | return | |
| 222 | - | } | |
| 223 | - | m.setStatus(FilterStatus{State: StateError, Message: "Compilation failed"}) | |
| 224 | - | return | |
| 225 | - | } | |
| 226 | - | if filter == nil { | |
| 227 | - | log.Error().Msg("filter compilation returned nil filter") | |
| 228 | - | if m.hasActiveFilter() { | |
| 229 | - | version := m.getCachedVersion() | |
| 230 | - | m.setStatus(FilterStatus{ | |
| 231 | - | State: StateActive, | |
| 232 | - | Message: "Filters active (update skipped)", | |
| 233 | - | Version: version, | |
| 234 | - | }) | |
| 235 | - | return | |
| 236 | - | } | |
| 237 | - | m.setStatus(FilterStatus{State: StateError, Message: "Compilation failed"}) | |
| 238 | - | return | |
| 239 | - | } | |
| 165 | + | func (m *Manager) loadFromCache(ctx context.Context) bool { | |
| 166 | + | log := logging.FromContext(ctx).With(). | |
| 167 | + | Str("component", "filter-manager"). | |
| 168 | + | Logger() | |
| 169 | + | ||
| 170 | + | m.setStatus(FilterStatus{State: StateLoading, Message: "Loading filters..."}) | |
| 171 | + | if !m.store.HasCompiledFilter(ctx, FilterIdentifier) { | |
| 172 | + | return false | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | log.Debug().Msg("found compiled filter, loading from cache") | |
| 176 | + | filter, err := m.store.Load(ctx, FilterIdentifier) | |
| 177 | + | if err != nil || filter == nil { | |
| 178 | + | log.Warn().Err(err).Msg("failed to load cached filter, will download") | |
| 179 | + | return false | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | version := m.setActiveFilter(filter, "Filters active") | |
| 183 | + | log.Info().Str("version", version).Msg("filters loaded from cache") | |
| 184 | + | return true | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | func (m *Manager) downloadCompileAndActivate(ctx context.Context) { | |
| 188 | + | log := logging.FromContext(ctx).With(). | |
| 189 | + | Str("component", "filter-manager"). | |
| 190 | + | Logger() | |
| 191 | + | ||
| 192 | + | paths, err := m.downloadFiltersWithProgress(ctx) | |
| 193 | + | if err != nil { | |
| 194 | + | log.Error().Err(err).Msg("failed to download filters") | |
| 195 | + | m.setStatus(FilterStatus{State: StateError, Message: "Download failed"}) | |
| 196 | + | return | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | validateErr := m.validateDownloadedFiles(ctx, paths) | |
| 200 | + | if validateErr != nil { | |
| 201 | + | log.Error().Err(validateErr).Msg("downloaded filter validation failed") | |
| 202 | + | m.setStatus(FilterStatus{State: StateError, Message: "Invalid downloaded filters"}) | |
| 203 | + | return | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | m.setStatus(FilterStatus{State: StateLoading, Message: "Compiling filters..."}) | |
| 207 | + | mergedPath, err := m.mergeJSONFiles(ctx, paths) | |
| 208 | + | if err != nil { | |
| 209 | + | log.Error().Err(err).Msg("failed to merge filter files") | |
| 210 | + | m.setStatus(FilterStatus{State: StateError, Message: "Filter merge failed"}) | |
| 211 | + | return | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | filter, err := m.store.Compile(ctx, FilterIdentifier, mergedPath) | |
| 215 | + | if err != nil { | |
| 216 | + | log.Error().Err(err).Msg("failed to compile filters") | |
| 217 | + | m.handleCompilationFailure() | |
| 218 | + | return | |
| 219 | + | } | |
| 220 | + | if filter == nil { | |
| 221 | + | log.Error().Msg("filter compilation returned nil filter") | |
| 222 | + | m.handleCompilationFailure() | |
| 223 | + | return | |
| 224 | + | } | |
| 240 | 225 | ||
| 241 | - | m.filterMu.Lock() | |
| 242 | - | m.filter = filter | |
| 243 | - | m.filterMu.Unlock() | |
| 226 | + | version := m.setActiveFilter(filter, "Filters active") | |
| 227 | + | log.Info().Str("version", version).Msg("filters compiled and active") | |
| 228 | + | } | |
| 244 | 229 | ||
| 230 | + | func (m *Manager) downloadFiltersWithProgress(ctx context.Context) ([]string, error) { | |
| 231 | + | m.setStatus(FilterStatus{State: StateLoading, Message: "Downloading filters..."}) | |
| 232 | + | return m.downloader.DownloadFilters(ctx, func(p DownloadProgress) { | |
| 233 | + | msg := fmt.Sprintf("Downloading filters (%d/%d)...", p.Current, p.Total) | |
| 234 | + | m.setStatus(FilterStatus{State: StateLoading, Message: msg}) | |
| 235 | + | }) | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | func (m *Manager) handleCompilationFailure() { | |
| 239 | + | if m.hasActiveFilter() { | |
| 245 | 240 | version := m.getCachedVersion() | |
| 246 | 241 | m.setStatus(FilterStatus{ | |
| 247 | 242 | State: StateActive, | |
| 248 | - | Message: "Filters active", | |
| 243 | + | Message: "Filters active (update skipped)", | |
| 249 | 244 | Version: version, | |
| 250 | 245 | }) | |
| 251 | - | log.Info().Str("version", version).Msg("filters compiled and active") | |
| 252 | - | }() | |
| 246 | + | return | |
| 247 | + | } | |
| 248 | + | m.setStatus(FilterStatus{State: StateError, Message: "Compilation failed"}) | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | func (m *Manager) setActiveFilter(filter *webkit.UserContentFilter, message string) string { | |
| 252 | + | m.filterMu.Lock() | |
| 253 | + | m.filter = filter | |
| 254 | + | m.filterMu.Unlock() | |
| 255 | + | ||
| 256 | + | version := m.getCachedVersion() | |
| 257 | + | m.setStatus(FilterStatus{ | |
| 258 | + | State: StateActive, | |
| 259 | + | Message: message, | |
| 260 | + | Version: version, | |
| 261 | + | }) | |
| 262 | + | return version | |
| 253 | 263 | } | |
| 254 | 264 | ||
| 255 | 265 | // mergeJSONFiles combines multiple JSON rule files into one. | |
| @@ -375,8 +385,9 @@ func (m *Manager) CheckForUpdates(ctx context.Context) error { | |||
| 375 | 385 | if err != nil { | |
| 376 | 386 | return fmt.Errorf("failed to download filter update: %w", err) | |
| 377 | 387 | } | |
| 378 | - | if err := m.validateDownloadedFiles(ctx, paths); err != nil { | |
| 379 | - | log.Warn().Err(err).Msg("invalid downloaded filters, keeping existing active filter") | |
| 388 | + | validateErr := m.validateDownloadedFiles(ctx, paths) | |
| 389 | + | if validateErr != nil { | |
| 390 | + | log.Warn().Err(validateErr).Msg("invalid downloaded filters, keeping existing active filter") | |
| 380 | 391 | return nil | |
| 381 | 392 | } | |
| 382 | 393 | ||
| @@ -422,7 +433,7 @@ func (m *Manager) validateDownloadedFiles(ctx context.Context, paths []string) e | |||
| 422 | 433 | for _, path := range paths { | |
| 423 | 434 | if err := validateRuleFile(path); err != nil { | |
| 424 | 435 | if quarantineErr := m.quarantineInvalidFile(ctx, path, "rule_validation_failed"); quarantineErr != nil { | |
| 425 | - | return fmt.Errorf("invalid filter file %s (%v), quarantine failed: %w", path, err, quarantineErr) | |
| 436 | + | return fmt.Errorf("invalid filter file %s: %w (quarantine failed: %s)", path, err, quarantineErr.Error()) | |
| 426 | 437 | } | |
| 427 | 438 | return fmt.Errorf("invalid filter file %s: %w", path, err) | |
| 428 | 439 | } | |
| @@ -467,7 +478,7 @@ func validateRuleFile(path string) error { | |||
| 467 | 478 | return nil | |
| 468 | 479 | } | |
| 469 | 480 | ||
| 470 | - | func (m *Manager) quarantineInvalidFile(ctx context.Context, srcPath string, cause string) error { | |
| 481 | + | func (m *Manager) quarantineInvalidFile(ctx context.Context, srcPath, cause string) error { | |
| 471 | 482 | log := logging.FromContext(ctx).With(). | |
| 472 | 483 | Str("component", "filter-manager"). | |
| 473 | 484 | Logger() | |
| @@ -141,115 +141,125 @@ func (m *Manager) LoadAsync(ctx context.Context) { | |||||
| 141 | return | 141 | return | ||
| 142 | } | 142 | } | ||
| 143 | 143 | ||||
| 144 | - | go func() { | 144 | + | go m.loadAsyncWorker(ctx) |
| 145 | - | log := logging.FromContext(ctx).With(). | 145 | + | } |
| 146 | - | Str("component", "filter-manager"). | |||
| 147 | - | Logger() | |||
| 148 | - | ||||
| 149 | - | // If already loaded, skip | |||
| 150 | - | m.filterMu.RLock() | |||
| 151 | - | if m.filter != nil { | |||
| 152 | - | m.filterMu.RUnlock() | |||
| 153 | - | log.Debug().Msg("filters already loaded, skipping async load") | |||
| 154 | - | return | |||
| 155 | - | } | |||
| 156 | - | m.filterMu.RUnlock() | |||
| 157 | - | ||||
| 158 | - | // Try to load from cache first (fast path) | |||
| 159 | - | m.setStatus(FilterStatus{State: StateLoading, Message: "Loading filters..."}) | |||
| 160 | - | if m.store.HasCompiledFilter(ctx, FilterIdentifier) { | |||
| 161 | - | log.Debug().Msg("found compiled filter, loading from cache") | |||
| 162 | - | filter, err := m.store.Load(ctx, FilterIdentifier) | |||
| 163 | - | if err == nil && filter != nil { | |||
| 164 | - | m.filterMu.Lock() | |||
| 165 | - | m.filter = filter | |||
| 166 | - | m.filterMu.Unlock() | |||
| 167 | - | ||||
| 168 | - | version := m.getCachedVersion() | |||
| 169 | - | m.setStatus(FilterStatus{ | |||
| 170 | - | State: StateActive, | |||
| 171 | - | Message: "Filters active", | |||
| 172 | - | Version: version, | |||
| 173 | - | }) | |||
| 174 | - | log.Info().Str("version", version).Msg("filters loaded from cache") | |||
| 175 | - | ||||
| 176 | - | m.checkStaleCacheAndUpdate(ctx) | |||
| 177 | - | return | |||
| 178 | - | } | |||
| 179 | - | log.Warn().Err(err).Msg("failed to load cached filter, will download") | |||
| 180 | - | } | |||
| 181 | 146 | ||||
| 182 | - | // Download filters | 147 | + | func (m *Manager) loadAsyncWorker(ctx context.Context) { |
| 183 | - | m.setStatus(FilterStatus{State: StateLoading, Message: "Downloading filters..."}) | 148 | + | log := logging.FromContext(ctx).With(). |
| 184 | - | paths, err := m.downloader.DownloadFilters(ctx, func(p DownloadProgress) { | 149 | + | Str("component", "filter-manager"). |
| 185 | - | msg := fmt.Sprintf("Downloading filters (%d/%d)...", p.Current, p.Total) | 150 | + | Logger() |
| 186 | - | m.setStatus(FilterStatus{State: StateLoading, Message: msg}) | |||
| 187 | - | }) | |||
| 188 | - | if err != nil { | |||
| 189 | - | log.Error().Err(err).Msg("failed to download filters") | |||
| 190 | - | m.setStatus(FilterStatus{State: StateError, Message: "Download failed"}) | |||
| 191 | - | return | |||
| 192 | - | } | |||
| 193 | 151 | ||||
| 194 | - | if err := m.validateDownloadedFiles(ctx, paths); err != nil { | 152 | + | if m.hasActiveFilter() { |
| 195 | - | log.Error().Err(err).Msg("downloaded filter validation failed") | 153 | + | log.Debug().Msg("filters already loaded, skipping async load") |
| 196 | - | m.setStatus(FilterStatus{State: StateError, Message: "Invalid downloaded filters"}) | 154 | + | return |
| 197 | - | return | 155 | + | } |
| 198 | - | } | |||
| 199 | 156 | ||||
| 200 | - | m.setStatus(FilterStatus{State: StateLoading, Message: "Compiling filters..."}) | 157 | + | if m.loadFromCache(ctx) { |
| 158 | + | m.checkStaleCacheAndUpdate(ctx) | |||
| 159 | + | return | |||
| 160 | + | } | |||
| 201 | 161 | ||||
| 202 | - | // Merge JSON files into one for compilation | 162 | + | m.downloadCompileAndActivate(ctx) |
| 203 | - | mergedPath, err := m.mergeJSONFiles(ctx, paths) | 163 | + | } |
| 204 | - | if err != nil { | |||
| 205 | - | log.Error().Err(err).Msg("failed to merge filter files") | |||
| 206 | - | m.setStatus(FilterStatus{State: StateError, Message: "Filter merge failed"}) | |||
| 207 | - | return | |||
| 208 | - | } | |||
| 209 | 164 | ||||
| 210 | - | // Compile the merged filter | 165 | + | func (m *Manager) loadFromCache(ctx context.Context) bool { |
| 211 | - | filter, err := m.store.Compile(ctx, FilterIdentifier, mergedPath) | 166 | + | log := logging.FromContext(ctx).With(). |
| 212 | - | if err != nil { | 167 | + | Str("component", "filter-manager"). |
| 213 | - | log.Error().Err(err).Msg("failed to compile filters") | 168 | + | Logger() |
| 214 | - | if m.hasActiveFilter() { | 169 | + | |
| 215 | - | version := m.getCachedVersion() | 170 | + | m.setStatus(FilterStatus{State: StateLoading, Message: "Loading filters..."}) |
| 216 | - | m.setStatus(FilterStatus{ | 171 | + | if !m.store.HasCompiledFilter(ctx, FilterIdentifier) { |
| 217 | - | State: StateActive, | 172 | + | return false |
| 218 | - | Message: "Filters active (update skipped)", | 173 | + | } |
| 219 | - | Version: version, | 174 | + | |
| 220 | - | }) | 175 | + | log.Debug().Msg("found compiled filter, loading from cache") |
| 221 | - | return | 176 | + | filter, err := m.store.Load(ctx, FilterIdentifier) |
| 222 | - | } | 177 | + | if err != nil || filter == nil { |
| 223 | - | m.setStatus(FilterStatus{State: StateError, Message: "Compilation failed"}) | 178 | + | log.Warn().Err(err).Msg("failed to load cached filter, will download") |
| 224 | - | return | 179 | + | return false |
| 225 | - | } | 180 | + | } |
| 226 | - | if filter == nil { | 181 | + | |
| 227 | - | log.Error().Msg("filter compilation returned nil filter") | 182 | + | version := m.setActiveFilter(filter, "Filters active") |
| 228 | - | if m.hasActiveFilter() { | 183 | + | log.Info().Str("version", version).Msg("filters loaded from cache") |
| 229 | - | version := m.getCachedVersion() | 184 | + | return true |
| 230 | - | m.setStatus(FilterStatus{ | 185 | + | } |
| 231 | - | State: StateActive, | 186 | + | |
| 232 | - | Message: "Filters active (update skipped)", | 187 | + | func (m *Manager) downloadCompileAndActivate(ctx context.Context) { |
| 233 | - | Version: version, | 188 | + | log := logging.FromContext(ctx).With(). |
| 234 | - | }) | 189 | + | Str("component", "filter-manager"). |
| 235 | - | return | 190 | + | Logger() |
| 236 | - | } | 191 | + | |
| 237 | - | m.setStatus(FilterStatus{State: StateError, Message: "Compilation failed"}) | 192 | + | paths, err := m.downloadFiltersWithProgress(ctx) |
| 238 | - | return | 193 | + | if err != nil { |
| 239 | - | } | 194 | + | log.Error().Err(err).Msg("failed to download filters") |
| 195 | + | m.setStatus(FilterStatus{State: StateError, Message: "Download failed"}) | |||
| 196 | + | return | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | validateErr := m.validateDownloadedFiles(ctx, paths) | |||
| 200 | + | if validateErr != nil { | |||
| 201 | + | log.Error().Err(validateErr).Msg("downloaded filter validation failed") | |||
| 202 | + | m.setStatus(FilterStatus{State: StateError, Message: "Invalid downloaded filters"}) | |||
| 203 | + | return | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | m.setStatus(FilterStatus{State: StateLoading, Message: "Compiling filters..."}) | |||
| 207 | + | mergedPath, err := m.mergeJSONFiles(ctx, paths) | |||
| 208 | + | if err != nil { | |||
| 209 | + | log.Error().Err(err).Msg("failed to merge filter files") | |||
| 210 | + | m.setStatus(FilterStatus{State: StateError, Message: "Filter merge failed"}) | |||
| 211 | + | return | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | filter, err := m.store.Compile(ctx, FilterIdentifier, mergedPath) | |||
| 215 | + | if err != nil { | |||
| 216 | + | log.Error().Err(err).Msg("failed to compile filters") | |||
| 217 | + | m.handleCompilationFailure() | |||
| 218 | + | return | |||
| 219 | + | } | |||
| 220 | + | if filter == nil { | |||
| 221 | + | log.Error().Msg("filter compilation returned nil filter") | |||
| 222 | + | m.handleCompilationFailure() | |||
| 223 | + | return | |||
| 224 | + | } | |||
| 240 | 225 | ||||
| 241 | - | m.filterMu.Lock() | 226 | + | version := m.setActiveFilter(filter, "Filters active") |
| 242 | - | m.filter = filter | 227 | + | log.Info().Str("version", version).Msg("filters compiled and active") |
| 243 | - | m.filterMu.Unlock() | 228 | + | } |
| 244 | 229 | ||||
| 230 | + | func (m *Manager) downloadFiltersWithProgress(ctx context.Context) ([]string, error) { | |||
| 231 | + | m.setStatus(FilterStatus{State: StateLoading, Message: "Downloading filters..."}) | |||
| 232 | + | return m.downloader.DownloadFilters(ctx, func(p DownloadProgress) { | |||
| 233 | + | msg := fmt.Sprintf("Downloading filters (%d/%d)...", p.Current, p.Total) | |||
| 234 | + | m.setStatus(FilterStatus{State: StateLoading, Message: msg}) | |||
| 235 | + | }) | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | func (m *Manager) handleCompilationFailure() { | |||
| 239 | + | if m.hasActiveFilter() { | |||
| 245 | version := m.getCachedVersion() | 240 | version := m.getCachedVersion() | ||
| 246 | m.setStatus(FilterStatus{ | 241 | m.setStatus(FilterStatus{ | ||
| 247 | State: StateActive, | 242 | State: StateActive, | ||
| 248 | - | Message: "Filters active", | 243 | + | Message: "Filters active (update skipped)", |
| 249 | Version: version, | 244 | Version: version, | ||
| 250 | }) | 245 | }) | ||
| 251 | - | log.Info().Str("version", version).Msg("filters compiled and active") | 246 | + | return |
| 252 | - | }() | 247 | + | } |
| 248 | + | m.setStatus(FilterStatus{State: StateError, Message: "Compilation failed"}) | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | func (m *Manager) setActiveFilter(filter *webkit.UserContentFilter, message string) string { | |||
| 252 | + | m.filterMu.Lock() | |||
| 253 | + | m.filter = filter | |||
| 254 | + | m.filterMu.Unlock() | |||
| 255 | + | ||||
| 256 | + | version := m.getCachedVersion() | |||
| 257 | + | m.setStatus(FilterStatus{ | |||
| 258 | + | State: StateActive, | |||
| 259 | + | Message: message, | |||
| 260 | + | Version: version, | |||
| 261 | + | }) | |||
| 262 | + | return version | |||
| 253 | } | 263 | } | ||
| 254 | 264 | ||||
| 255 | // mergeJSONFiles combines multiple JSON rule files into one. | 265 | // mergeJSONFiles combines multiple JSON rule files into one. | ||
| @@ -375,8 +385,9 @@ func (m *Manager) CheckForUpdates(ctx context.Context) error { | |||||
| 375 | if err != nil { | 385 | if err != nil { | ||
| 376 | return fmt.Errorf("failed to download filter update: %w", err) | 386 | return fmt.Errorf("failed to download filter update: %w", err) | ||
| 377 | } | 387 | } | ||
| 378 | - | if err := m.validateDownloadedFiles(ctx, paths); err != nil { | 388 | + | validateErr := m.validateDownloadedFiles(ctx, paths) |
| 379 | - | log.Warn().Err(err).Msg("invalid downloaded filters, keeping existing active filter") | 389 | + | if validateErr != nil { |
| 390 | + | log.Warn().Err(validateErr).Msg("invalid downloaded filters, keeping existing active filter") | |||
| 380 | return nil | 391 | return nil | ||
| 381 | } | 392 | } | ||
| 382 | 393 | ||||
| @@ -422,7 +433,7 @@ func (m *Manager) validateDownloadedFiles(ctx context.Context, paths []string) e | |||||
| 422 | for _, path := range paths { | 433 | for _, path := range paths { | ||
| 423 | if err := validateRuleFile(path); err != nil { | 434 | if err := validateRuleFile(path); err != nil { | ||
| 424 | if quarantineErr := m.quarantineInvalidFile(ctx, path, "rule_validation_failed"); quarantineErr != nil { | 435 | if quarantineErr := m.quarantineInvalidFile(ctx, path, "rule_validation_failed"); quarantineErr != nil { | ||
| 425 | - | return fmt.Errorf("invalid filter file %s (%v), quarantine failed: %w", path, err, quarantineErr) | 436 | + | return fmt.Errorf("invalid filter file %s: %w (quarantine failed: %s)", path, err, quarantineErr.Error()) |
| 426 | } | 437 | } | ||
| 427 | return fmt.Errorf("invalid filter file %s: %w", path, err) | 438 | return fmt.Errorf("invalid filter file %s: %w", path, err) | ||
| 428 | } | 439 | } | ||
| @@ -467,7 +478,7 @@ func validateRuleFile(path string) error { | |||||
| 467 | return nil | 478 | return nil | ||
| 468 | } | 479 | } | ||
| 469 | 480 | ||||
| 470 | - | func (m *Manager) quarantineInvalidFile(ctx context.Context, srcPath string, cause string) error { | 481 | + | func (m *Manager) quarantineInvalidFile(ctx context.Context, srcPath, cause string) error { |
| 471 | log := logging.FromContext(ctx).With(). | 482 | log := logging.FromContext(ctx).With(). | ||
| 472 | Str("component", "filter-manager"). | 483 | Str("component", "filter-manager"). | ||
| 473 | Logger() | 484 | Logger() | ||
M internal/infrastructure/filtering/manager_test.go +1 -1
| @@ -538,7 +538,7 @@ func TestManager_LoadAsync_QuarantinesInvalidDownloadedPayload(t *testing.T) { | |||
| 538 | 538 | assert.Equal(t, "Invalid downloaded filters", mgr.Status().Message) | |
| 539 | 539 | ||
| 540 | 540 | _, statErr := os.Stat(invalidJSONFile) | |
| 541 | - | assert.ErrorIs(t, statErr, os.ErrNotExist) | |
| 541 | + | require.ErrorIs(t, statErr, os.ErrNotExist) | |
| 542 | 542 | ||
| 543 | 543 | quarantineDir := filepath.Join(jsonDir, "quarantine") | |
| 544 | 544 | entries, err := os.ReadDir(quarantineDir) | |
| @@ -538,7 +538,7 @@ func TestManager_LoadAsync_QuarantinesInvalidDownloadedPayload(t *testing.T) { | |||||
| 538 | assert.Equal(t, "Invalid downloaded filters", mgr.Status().Message) | 538 | assert.Equal(t, "Invalid downloaded filters", mgr.Status().Message) | ||
| 539 | 539 | ||||
| 540 | _, statErr := os.Stat(invalidJSONFile) | 540 | _, statErr := os.Stat(invalidJSONFile) | ||
| 541 | - | assert.ErrorIs(t, statErr, os.ErrNotExist) | 541 | + | require.ErrorIs(t, statErr, os.ErrNotExist) |
| 542 | 542 | ||||
| 543 | quarantineDir := filepath.Join(jsonDir, "quarantine") | 543 | quarantineDir := filepath.Join(jsonDir, "quarantine") | ||
| 544 | entries, err := os.ReadDir(quarantineDir) | 544 | entries, err := os.ReadDir(quarantineDir) | ||
M internal/infrastructure/webkit/scheme_handler.go +4 -3
| @@ -31,6 +31,7 @@ const ( | |||
| 31 | 31 | ErrorPath = "error" | |
| 32 | 32 | CrashPath = "crash" | |
| 33 | 33 | IndexHTML = "index.html" | |
| 34 | + | httpGET = "GET" | |
| 34 | 35 | ) | |
| 35 | 36 | ||
| 36 | 37 | // SchemeRequest represents a request to a custom URI scheme. | |
| @@ -165,7 +166,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||
| 165 | 166 | ||
| 166 | 167 | // Crash page (web process termination fallback) | |
| 167 | 168 | h.RegisterPage("/crash", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | |
| 168 | - | if req.Method != "" && req.Method != "GET" { | |
| 169 | + | if req.Method != "" && req.Method != httpGET { | |
| 169 | 170 | return nil | |
| 170 | 171 | } | |
| 171 | 172 | originalURI := crashOriginalURI(req.URI) | |
| @@ -178,7 +179,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||
| 178 | 179 | ||
| 179 | 180 | // API: Get current config (used by dumb://config) | |
| 180 | 181 | h.RegisterPage("/api/config", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | |
| 181 | - | if req.Method != "GET" { | |
| 182 | + | if req.Method != httpGET { | |
| 182 | 183 | return nil | |
| 183 | 184 | } | |
| 184 | 185 | ||
| @@ -187,7 +188,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||
| 187 | 188 | ||
| 188 | 189 | // API: Get default config (used by Reset Defaults in dumb://config) | |
| 189 | 190 | h.RegisterPage("/api/config/default", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | |
| 190 | - | if req.Method != "GET" { | |
| 191 | + | if req.Method != httpGET { | |
| 191 | 192 | return nil | |
| 192 | 193 | } | |
| 193 | 194 | ||
| @@ -31,6 +31,7 @@ const ( | |||||
| 31 | ErrorPath = "error" | 31 | ErrorPath = "error" | ||
| 32 | CrashPath = "crash" | 32 | CrashPath = "crash" | ||
| 33 | IndexHTML = "index.html" | 33 | IndexHTML = "index.html" | ||
| 34 | + | httpGET = "GET" | |||
| 34 | ) | 35 | ) | ||
| 35 | 36 | ||||
| 36 | // SchemeRequest represents a request to a custom URI scheme. | 37 | // SchemeRequest represents a request to a custom URI scheme. | ||
| @@ -165,7 +166,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||||
| 165 | 166 | ||||
| 166 | // Crash page (web process termination fallback) | 167 | // Crash page (web process termination fallback) | ||
| 167 | h.RegisterPage("/crash", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | 168 | h.RegisterPage("/crash", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | ||
| 168 | - | if req.Method != "" && req.Method != "GET" { | 169 | + | if req.Method != "" && req.Method != httpGET { |
| 169 | return nil | 170 | return nil | ||
| 170 | } | 171 | } | ||
| 171 | originalURI := crashOriginalURI(req.URI) | 172 | originalURI := crashOriginalURI(req.URI) | ||
| @@ -178,7 +179,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||||
| 178 | 179 | ||||
| 179 | // API: Get current config (used by dumb://config) | 180 | // API: Get current config (used by dumb://config) | ||
| 180 | h.RegisterPage("/api/config", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | 181 | h.RegisterPage("/api/config", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | ||
| 181 | - | if req.Method != "GET" { | 182 | + | if req.Method != httpGET { |
| 182 | return nil | 183 | return nil | ||
| 183 | } | 184 | } | ||
| 184 | 185 | ||||
| @@ -187,7 +188,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||||
| 187 | 188 | ||||
| 188 | // API: Get default config (used by Reset Defaults in dumb://config) | 189 | // API: Get default config (used by Reset Defaults in dumb://config) | ||
| 189 | h.RegisterPage("/api/config/default", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | 190 | h.RegisterPage("/api/config/default", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | ||
| 190 | - | if req.Method != "GET" { | 191 | + | if req.Method != httpGET { |
| 191 | return nil | 192 | return nil | ||
| 192 | } | 193 | } | ||
| 193 | 194 | ||||
M internal/infrastructure/webkit/scheme_handler_test.go +2 -2
| @@ -10,8 +10,8 @@ import ( | |||
| 10 | 10 | ||
| 11 | 11 | func TestCrashOriginalURI(t *testing.T) { | |
| 12 | 12 | assert.Equal(t, "https://example.com/path?q=1", crashOriginalURI("dumb://home/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | |
| 13 | - | assert.Equal(t, "", crashOriginalURI("dumb://home/crash")) | |
| 14 | - | assert.Equal(t, "", crashOriginalURI("%%%")) | |
| 13 | + | assert.Empty(t, crashOriginalURI("dumb://home/crash")) | |
| 14 | + | assert.Empty(t, crashOriginalURI("%%%")) | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | 17 | func TestBuildCrashPageHTMLIncludesReloadTarget(t *testing.T) { | |
| @@ -10,8 +10,8 @@ import ( | |||||
| 10 | 10 | ||||
| 11 | func TestCrashOriginalURI(t *testing.T) { | 11 | func TestCrashOriginalURI(t *testing.T) { | ||
| 12 | assert.Equal(t, "https://example.com/path?q=1", crashOriginalURI("dumb://home/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | 12 | assert.Equal(t, "https://example.com/path?q=1", crashOriginalURI("dumb://home/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | ||
| 13 | - | assert.Equal(t, "", crashOriginalURI("dumb://home/crash")) | 13 | + | assert.Empty(t, crashOriginalURI("dumb://home/crash")) |
| 14 | - | assert.Equal(t, "", crashOriginalURI("%%%")) | 14 | + | assert.Empty(t, crashOriginalURI("%%%")) |
| 15 | } | 15 | } | ||
| 16 | 16 | ||||
| 17 | func TestBuildCrashPageHTMLIncludesReloadTarget(t *testing.T) { | 17 | func TestBuildCrashPageHTMLIncludesReloadTarget(t *testing.T) { | ||
M internal/ui/coordinator/content.go +24 -25
| @@ -802,9 +802,9 @@ func (c *ContentCoordinator) onTitleChanged(ctx context.Context, paneID entity.P | |||
| 802 | 802 | // Notify history persistence (get URL from WebView) | |
| 803 | 803 | if c.onTitleUpdated != nil { | |
| 804 | 804 | if wv := c.webViews[paneID]; wv != nil { | |
| 805 | - | url := wv.URI() | |
| 806 | - | if url != "" && title != "" { | |
| 807 | - | c.onTitleUpdated(ctx, paneID, url, title) | |
| 805 | + | currentURI := wv.URI() | |
| 806 | + | if currentURI != "" && title != "" { | |
| 807 | + | c.onTitleUpdated(ctx, paneID, currentURI, title) | |
| 808 | 808 | } | |
| 809 | 809 | } | |
| 810 | 810 | } | |
| @@ -912,22 +912,22 @@ func (c *ContentCoordinator) FaviconAdapter() *adapter.FaviconAdapter { | |||
| 912 | 912 | // SetNavigationOrigin records the original URL before navigation starts. | |
| 913 | 913 | // This allows caching favicons under both original and final domains | |
| 914 | 914 | // when cross-domain redirects occur (e.g., google.fr → google.com). | |
| 915 | - | func (c *ContentCoordinator) SetNavigationOrigin(paneID entity.PaneID, url string) { | |
| 915 | + | func (c *ContentCoordinator) SetNavigationOrigin(paneID entity.PaneID, uri string) { | |
| 916 | 916 | c.navOriginMu.Lock() | |
| 917 | - | c.navOrigins[paneID] = url | |
| 917 | + | c.navOrigins[paneID] = uri | |
| 918 | 918 | c.navOriginMu.Unlock() | |
| 919 | 919 | } | |
| 920 | 920 | ||
| 921 | 921 | // PreloadCachedFavicon checks the favicon cache and updates the stacked pane | |
| 922 | 922 | // title bar immediately if a cached favicon exists for the URL. | |
| 923 | 923 | // This provides instant favicon display without waiting for WebKit. | |
| 924 | - | func (c *ContentCoordinator) PreloadCachedFavicon(ctx context.Context, paneID entity.PaneID, url string) { | |
| 925 | - | if c.faviconAdapter == nil || url == "" { | |
| 924 | + | func (c *ContentCoordinator) PreloadCachedFavicon(ctx context.Context, paneID entity.PaneID, uri string) { | |
| 925 | + | if c.faviconAdapter == nil || uri == "" { | |
| 926 | 926 | return | |
| 927 | 927 | } | |
| 928 | 928 | ||
| 929 | 929 | // Check memory and disk cache (no external fetch) | |
| 930 | - | texture := c.faviconAdapter.PreloadFromCache(url) | |
| 930 | + | texture := c.faviconAdapter.PreloadFromCache(uri) | |
| 931 | 931 | if texture == nil { | |
| 932 | 932 | return | |
| 933 | 933 | } | |
| @@ -953,14 +953,14 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||
| 953 | 953 | log := logging.FromContext(ctx) | |
| 954 | 954 | logging.Trace().Mark("load_committed") | |
| 955 | 955 | ||
| 956 | - | url := wv.URI() | |
| 957 | - | if url == "" { | |
| 956 | + | uri := wv.URI() | |
| 957 | + | if uri == "" { | |
| 958 | 958 | return | |
| 959 | 959 | } | |
| 960 | 960 | ||
| 961 | 961 | // Set appropriate background color based on page type to prevent dark background bleeding. | |
| 962 | 962 | switch { | |
| 963 | - | case strings.HasPrefix(url, "dumb://"): | |
| 963 | + | case strings.HasPrefix(uri, "dumb://"): | |
| 964 | 964 | // Internal pages: apply themed background | |
| 965 | 965 | theme, ok := c.getCurrentTheme() | |
| 966 | 966 | if ok && theme.prefersDark { | |
| @@ -968,7 +968,7 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||
| 968 | 968 | } else { | |
| 969 | 969 | wv.ResetBackgroundToDefault() | |
| 970 | 970 | } | |
| 971 | - | case strings.HasPrefix(url, "about:"): | |
| 971 | + | case strings.HasPrefix(uri, "about:"): | |
| 972 | 972 | // Keep pool background (no action) | |
| 973 | 973 | default: | |
| 974 | 974 | // External pages: white background | |
| @@ -980,7 +980,7 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||
| 980 | 980 | // Skip showing if this is about:blank but the pane is loading a different URL | |
| 981 | 981 | // This prevents the brief flash of about:blank during initial navigation | |
| 982 | 982 | shouldShow := true | |
| 983 | - | if url == aboutBlankURI { | |
| 983 | + | if uri == aboutBlankURI { | |
| 984 | 984 | // Get the pane's intended URI from the workspace | |
| 985 | 985 | ws, _ := c.getActiveWS() | |
| 986 | 986 | if ws != nil { | |
| @@ -1011,15 +1011,15 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||
| 1011 | 1011 | ||
| 1012 | 1012 | c.markPendingReveal(paneID) | |
| 1013 | 1013 | if wv.EstimatedProgress() > 0 { | |
| 1014 | - | c.revealIfPending(ctx, paneID, url, "progress-after-commit") | |
| 1014 | + | c.revealIfPending(ctx, paneID, uri, "progress-after-commit") | |
| 1015 | 1015 | } | |
| 1016 | 1016 | ||
| 1017 | 1017 | // Update domain model with current URI for session snapshots | |
| 1018 | - | c.updatePaneURI(paneID, url) | |
| 1018 | + | c.updatePaneURI(paneID, uri) | |
| 1019 | 1019 | ||
| 1020 | 1020 | // Record history - URI is guaranteed to be correct at LoadCommitted | |
| 1021 | 1021 | if c.onHistoryRecord != nil { | |
| 1022 | - | c.onHistoryRecord(ctx, paneID, url) | |
| 1022 | + | c.onHistoryRecord(ctx, paneID, uri) | |
| 1023 | 1023 | } | |
| 1024 | 1024 | ||
| 1025 | 1025 | // Apply zoom | |
| @@ -1027,7 +1027,7 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||
| 1027 | 1027 | return | |
| 1028 | 1028 | } | |
| 1029 | 1029 | ||
| 1030 | - | domain, err := usecase.ExtractDomain(url) | |
| 1030 | + | domain, err := usecase.ExtractDomain(uri) | |
| 1031 | 1031 | if err != nil { | |
| 1032 | 1032 | return | |
| 1033 | 1033 | } | |
| @@ -1058,21 +1058,21 @@ func (c *ContentCoordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID | |||
| 1058 | 1058 | ||
| 1059 | 1059 | // onSPANavigation records history when URL changes via JavaScript (History API). | |
| 1060 | 1060 | // This handles SPA navigation like YouTube search, where the URL changes without a page load. | |
| 1061 | - | func (c *ContentCoordinator) onSPANavigation(ctx context.Context, paneID entity.PaneID, url string) { | |
| 1061 | + | func (c *ContentCoordinator) onSPANavigation(ctx context.Context, paneID entity.PaneID, uri string) { | |
| 1062 | 1062 | // Update domain model with current URI for session snapshots | |
| 1063 | - | c.updatePaneURI(paneID, url) | |
| 1063 | + | c.updatePaneURI(paneID, uri) | |
| 1064 | 1064 | ||
| 1065 | 1065 | // Record history for SPA navigation | |
| 1066 | 1066 | if c.onHistoryRecord != nil { | |
| 1067 | - | c.onHistoryRecord(ctx, paneID, url) | |
| 1067 | + | c.onHistoryRecord(ctx, paneID, uri) | |
| 1068 | 1068 | } | |
| 1069 | 1069 | } | |
| 1070 | 1070 | ||
| 1071 | 1071 | // updatePaneURI updates the pane's URI in the domain model. | |
| 1072 | 1072 | // This is called on navigation so that session snapshots capture the current URL. | |
| 1073 | - | func (c *ContentCoordinator) updatePaneURI(paneID entity.PaneID, url string) { | |
| 1073 | + | func (c *ContentCoordinator) updatePaneURI(paneID entity.PaneID, uri string) { | |
| 1074 | 1074 | if c.onPaneURIUpdated != nil { | |
| 1075 | - | c.onPaneURIUpdated(paneID, url) | |
| 1075 | + | c.onPaneURIUpdated(paneID, uri) | |
| 1076 | 1076 | } | |
| 1077 | 1077 | } | |
| 1078 | 1078 | ||
| @@ -1148,7 +1148,7 @@ func (c *ContentCoordinator) clearPendingReveal(paneID entity.PaneID) { | |||
| 1148 | 1148 | c.revealMu.Unlock() | |
| 1149 | 1149 | } | |
| 1150 | 1150 | ||
| 1151 | - | func (c *ContentCoordinator) revealIfPending(ctx context.Context, paneID entity.PaneID, url, reason string) { | |
| 1151 | + | func (c *ContentCoordinator) revealIfPending(ctx context.Context, paneID entity.PaneID, uri, reason string) { | |
| 1152 | 1152 | c.revealMu.Lock() | |
| 1153 | 1153 | pending := c.pendingReveal[paneID] | |
| 1154 | 1154 | if pending { | |
| @@ -1170,7 +1170,7 @@ func (c *ContentCoordinator) revealIfPending(ctx context.Context, paneID entity. | |||
| 1170 | 1170 | logging.FromContext(ctx). | |
| 1171 | 1171 | Debug(). | |
| 1172 | 1172 | Str("pane_id", string(paneID)). | |
| 1173 | - | Str("url", url). | |
| 1173 | + | Str("url", uri). | |
| 1174 | 1174 | Str("reason", reason). | |
| 1175 | 1175 | Msg("webview revealed") | |
| 1176 | 1176 | } | |
| @@ -1518,7 +1518,6 @@ func (c *ContentCoordinator) setupWebViewCallbacks(ctx context.Context, paneID e | |||
| 1518 | 1518 | ||
| 1519 | 1519 | // SPA navigation and external scheme handling | |
| 1520 | 1520 | wv.OnURIChanged = func(uri string) { | |
| 1521 | - | log := logging.FromContext(ctx) | |
| 1522 | 1521 | if uri == "" { | |
| 1523 | 1522 | return | |
| 1524 | 1523 | } | |
| @@ -802,9 +802,9 @@ func (c *ContentCoordinator) onTitleChanged(ctx context.Context, paneID entity.P | |||||
| 802 | // Notify history persistence (get URL from WebView) | 802 | // Notify history persistence (get URL from WebView) | ||
| 803 | if c.onTitleUpdated != nil { | 803 | if c.onTitleUpdated != nil { | ||
| 804 | if wv := c.webViews[paneID]; wv != nil { | 804 | if wv := c.webViews[paneID]; wv != nil { | ||
| 805 | - | url := wv.URI() | 805 | + | currentURI := wv.URI() |
| 806 | - | if url != "" && title != "" { | 806 | + | if currentURI != "" && title != "" { |
| 807 | - | c.onTitleUpdated(ctx, paneID, url, title) | 807 | + | c.onTitleUpdated(ctx, paneID, currentURI, title) |
| 808 | } | 808 | } | ||
| 809 | } | 809 | } | ||
| 810 | } | 810 | } | ||
| @@ -912,22 +912,22 @@ func (c *ContentCoordinator) FaviconAdapter() *adapter.FaviconAdapter { | |||||
| 912 | // SetNavigationOrigin records the original URL before navigation starts. | 912 | // SetNavigationOrigin records the original URL before navigation starts. | ||
| 913 | // This allows caching favicons under both original and final domains | 913 | // This allows caching favicons under both original and final domains | ||
| 914 | // when cross-domain redirects occur (e.g., google.fr → google.com). | 914 | // when cross-domain redirects occur (e.g., google.fr → google.com). | ||
| 915 | - | func (c *ContentCoordinator) SetNavigationOrigin(paneID entity.PaneID, url string) { | 915 | + | func (c *ContentCoordinator) SetNavigationOrigin(paneID entity.PaneID, uri string) { |
| 916 | c.navOriginMu.Lock() | 916 | c.navOriginMu.Lock() | ||
| 917 | - | c.navOrigins[paneID] = url | 917 | + | c.navOrigins[paneID] = uri |
| 918 | c.navOriginMu.Unlock() | 918 | c.navOriginMu.Unlock() | ||
| 919 | } | 919 | } | ||
| 920 | 920 | ||||
| 921 | // PreloadCachedFavicon checks the favicon cache and updates the stacked pane | 921 | // PreloadCachedFavicon checks the favicon cache and updates the stacked pane | ||
| 922 | // title bar immediately if a cached favicon exists for the URL. | 922 | // title bar immediately if a cached favicon exists for the URL. | ||
| 923 | // This provides instant favicon display without waiting for WebKit. | 923 | // This provides instant favicon display without waiting for WebKit. | ||
| 924 | - | func (c *ContentCoordinator) PreloadCachedFavicon(ctx context.Context, paneID entity.PaneID, url string) { | 924 | + | func (c *ContentCoordinator) PreloadCachedFavicon(ctx context.Context, paneID entity.PaneID, uri string) { |
| 925 | - | if c.faviconAdapter == nil || url == "" { | 925 | + | if c.faviconAdapter == nil || uri == "" { |
| 926 | return | 926 | return | ||
| 927 | } | 927 | } | ||
| 928 | 928 | ||||
| 929 | // Check memory and disk cache (no external fetch) | 929 | // Check memory and disk cache (no external fetch) | ||
| 930 | - | texture := c.faviconAdapter.PreloadFromCache(url) | 930 | + | texture := c.faviconAdapter.PreloadFromCache(uri) |
| 931 | if texture == nil { | 931 | if texture == nil { | ||
| 932 | return | 932 | return | ||
| 933 | } | 933 | } | ||
| @@ -953,14 +953,14 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||||
| 953 | log := logging.FromContext(ctx) | 953 | log := logging.FromContext(ctx) | ||
| 954 | logging.Trace().Mark("load_committed") | 954 | logging.Trace().Mark("load_committed") | ||
| 955 | 955 | ||||
| 956 | - | url := wv.URI() | 956 | + | uri := wv.URI() |
| 957 | - | if url == "" { | 957 | + | if uri == "" { |
| 958 | return | 958 | return | ||
| 959 | } | 959 | } | ||
| 960 | 960 | ||||
| 961 | // Set appropriate background color based on page type to prevent dark background bleeding. | 961 | // Set appropriate background color based on page type to prevent dark background bleeding. | ||
| 962 | switch { | 962 | switch { | ||
| 963 | - | case strings.HasPrefix(url, "dumb://"): | 963 | + | case strings.HasPrefix(uri, "dumb://"): |
| 964 | // Internal pages: apply themed background | 964 | // Internal pages: apply themed background | ||
| 965 | theme, ok := c.getCurrentTheme() | 965 | theme, ok := c.getCurrentTheme() | ||
| 966 | if ok && theme.prefersDark { | 966 | if ok && theme.prefersDark { | ||
| @@ -968,7 +968,7 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||||
| 968 | } else { | 968 | } else { | ||
| 969 | wv.ResetBackgroundToDefault() | 969 | wv.ResetBackgroundToDefault() | ||
| 970 | } | 970 | } | ||
| 971 | - | case strings.HasPrefix(url, "about:"): | 971 | + | case strings.HasPrefix(uri, "about:"): |
| 972 | // Keep pool background (no action) | 972 | // Keep pool background (no action) | ||
| 973 | default: | 973 | default: | ||
| 974 | // External pages: white background | 974 | // External pages: white background | ||
| @@ -980,7 +980,7 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||||
| 980 | // Skip showing if this is about:blank but the pane is loading a different URL | 980 | // Skip showing if this is about:blank but the pane is loading a different URL | ||
| 981 | // This prevents the brief flash of about:blank during initial navigation | 981 | // This prevents the brief flash of about:blank during initial navigation | ||
| 982 | shouldShow := true | 982 | shouldShow := true | ||
| 983 | - | if url == aboutBlankURI { | 983 | + | if uri == aboutBlankURI { |
| 984 | // Get the pane's intended URI from the workspace | 984 | // Get the pane's intended URI from the workspace | ||
| 985 | ws, _ := c.getActiveWS() | 985 | ws, _ := c.getActiveWS() | ||
| 986 | if ws != nil { | 986 | if ws != nil { | ||
| @@ -1011,15 +1011,15 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||||
| 1011 | 1011 | ||||
| 1012 | c.markPendingReveal(paneID) | 1012 | c.markPendingReveal(paneID) | ||
| 1013 | if wv.EstimatedProgress() > 0 { | 1013 | if wv.EstimatedProgress() > 0 { | ||
| 1014 | - | c.revealIfPending(ctx, paneID, url, "progress-after-commit") | 1014 | + | c.revealIfPending(ctx, paneID, uri, "progress-after-commit") |
| 1015 | } | 1015 | } | ||
| 1016 | 1016 | ||||
| 1017 | // Update domain model with current URI for session snapshots | 1017 | // Update domain model with current URI for session snapshots | ||
| 1018 | - | c.updatePaneURI(paneID, url) | 1018 | + | c.updatePaneURI(paneID, uri) |
| 1019 | 1019 | ||||
| 1020 | // Record history - URI is guaranteed to be correct at LoadCommitted | 1020 | // Record history - URI is guaranteed to be correct at LoadCommitted | ||
| 1021 | if c.onHistoryRecord != nil { | 1021 | if c.onHistoryRecord != nil { | ||
| 1022 | - | c.onHistoryRecord(ctx, paneID, url) | 1022 | + | c.onHistoryRecord(ctx, paneID, uri) |
| 1023 | } | 1023 | } | ||
| 1024 | 1024 | ||||
| 1025 | // Apply zoom | 1025 | // Apply zoom | ||
| @@ -1027,7 +1027,7 @@ func (c *ContentCoordinator) onLoadCommitted(ctx context.Context, paneID entity. | |||||
| 1027 | return | 1027 | return | ||
| 1028 | } | 1028 | } | ||
| 1029 | 1029 | ||||
| 1030 | - | domain, err := usecase.ExtractDomain(url) | 1030 | + | domain, err := usecase.ExtractDomain(uri) |
| 1031 | if err != nil { | 1031 | if err != nil { | ||
| 1032 | return | 1032 | return | ||
| 1033 | } | 1033 | } | ||
| @@ -1058,21 +1058,21 @@ func (c *ContentCoordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID | |||||
| 1058 | 1058 | ||||
| 1059 | // onSPANavigation records history when URL changes via JavaScript (History API). | 1059 | // onSPANavigation records history when URL changes via JavaScript (History API). | ||
| 1060 | // This handles SPA navigation like YouTube search, where the URL changes without a page load. | 1060 | // This handles SPA navigation like YouTube search, where the URL changes without a page load. | ||
| 1061 | - | func (c *ContentCoordinator) onSPANavigation(ctx context.Context, paneID entity.PaneID, url string) { | 1061 | + | func (c *ContentCoordinator) onSPANavigation(ctx context.Context, paneID entity.PaneID, uri string) { |
| 1062 | // Update domain model with current URI for session snapshots | 1062 | // Update domain model with current URI for session snapshots | ||
| 1063 | - | c.updatePaneURI(paneID, url) | 1063 | + | c.updatePaneURI(paneID, uri) |
| 1064 | 1064 | ||||
| 1065 | // Record history for SPA navigation | 1065 | // Record history for SPA navigation | ||
| 1066 | if c.onHistoryRecord != nil { | 1066 | if c.onHistoryRecord != nil { | ||
| 1067 | - | c.onHistoryRecord(ctx, paneID, url) | 1067 | + | c.onHistoryRecord(ctx, paneID, uri) |
| 1068 | } | 1068 | } | ||
| 1069 | } | 1069 | } | ||
| 1070 | 1070 | ||||
| 1071 | // updatePaneURI updates the pane's URI in the domain model. | 1071 | // updatePaneURI updates the pane's URI in the domain model. | ||
| 1072 | // This is called on navigation so that session snapshots capture the current URL. | 1072 | // This is called on navigation so that session snapshots capture the current URL. | ||
| 1073 | - | func (c *ContentCoordinator) updatePaneURI(paneID entity.PaneID, url string) { | 1073 | + | func (c *ContentCoordinator) updatePaneURI(paneID entity.PaneID, uri string) { |
| 1074 | if c.onPaneURIUpdated != nil { | 1074 | if c.onPaneURIUpdated != nil { | ||
| 1075 | - | c.onPaneURIUpdated(paneID, url) | 1075 | + | c.onPaneURIUpdated(paneID, uri) |
| 1076 | } | 1076 | } | ||
| 1077 | } | 1077 | } | ||
| 1078 | 1078 | ||||
| @@ -1148,7 +1148,7 @@ func (c *ContentCoordinator) clearPendingReveal(paneID entity.PaneID) { | |||||
| 1148 | c.revealMu.Unlock() | 1148 | c.revealMu.Unlock() | ||
| 1149 | } | 1149 | } | ||
| 1150 | 1150 | ||||
| 1151 | - | func (c *ContentCoordinator) revealIfPending(ctx context.Context, paneID entity.PaneID, url, reason string) { | 1151 | + | func (c *ContentCoordinator) revealIfPending(ctx context.Context, paneID entity.PaneID, uri, reason string) { |
| 1152 | c.revealMu.Lock() | 1152 | c.revealMu.Lock() | ||
| 1153 | pending := c.pendingReveal[paneID] | 1153 | pending := c.pendingReveal[paneID] | ||
| 1154 | if pending { | 1154 | if pending { | ||
| @@ -1170,7 +1170,7 @@ func (c *ContentCoordinator) revealIfPending(ctx context.Context, paneID entity. | |||||
| 1170 | logging.FromContext(ctx). | 1170 | logging.FromContext(ctx). | ||
| 1171 | Debug(). | 1171 | Debug(). | ||
| 1172 | Str("pane_id", string(paneID)). | 1172 | Str("pane_id", string(paneID)). | ||
| 1173 | - | Str("url", url). | 1173 | + | Str("url", uri). |
| 1174 | Str("reason", reason). | 1174 | Str("reason", reason). | ||
| 1175 | Msg("webview revealed") | 1175 | Msg("webview revealed") | ||
| 1176 | } | 1176 | } | ||
| @@ -1518,7 +1518,6 @@ func (c *ContentCoordinator) setupWebViewCallbacks(ctx context.Context, paneID e | |||||
| 1518 | 1518 | ||||
| 1519 | // SPA navigation and external scheme handling | 1519 | // SPA navigation and external scheme handling | ||
| 1520 | wv.OnURIChanged = func(uri string) { | 1520 | wv.OnURIChanged = func(uri string) { | ||
| 1521 | - | log := logging.FromContext(ctx) | |||
| 1522 | if uri == "" { | 1521 | if uri == "" { | ||
| 1523 | return | 1522 | return | ||
| 1524 | } | 1523 | } | ||
M internal/ui/coordinator/content_termination_test.go +1 -1
| @@ -16,7 +16,7 @@ func TestShouldRenderCrashPage(t *testing.T) { | |||
| 16 | 16 | ||
| 17 | 17 | func TestExtractOriginalURIFromCrashPage(t *testing.T) { | |
| 18 | 18 | assert.Equal(t, "https://example.com/path?q=1", extractOriginalURIFromCrashPage("dumb://home/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | |
| 19 | - | assert.Equal(t, "", extractOriginalURIFromCrashPage("dumb://home/crash")) | |
| 19 | + | assert.Empty(t, extractOriginalURIFromCrashPage("dumb://home/crash")) | |
| 20 | 20 | assert.Equal(t, "https://example.com", extractOriginalURIFromCrashPage("https://example.com")) | |
| 21 | 21 | assert.Equal(t, "%%%bad", extractOriginalURIFromCrashPage("%%%bad")) | |
| 22 | 22 | } | |
| @@ -16,7 +16,7 @@ func TestShouldRenderCrashPage(t *testing.T) { | |||||
| 16 | 16 | ||||
| 17 | func TestExtractOriginalURIFromCrashPage(t *testing.T) { | 17 | func TestExtractOriginalURIFromCrashPage(t *testing.T) { | ||
| 18 | assert.Equal(t, "https://example.com/path?q=1", extractOriginalURIFromCrashPage("dumb://home/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | 18 | assert.Equal(t, "https://example.com/path?q=1", extractOriginalURIFromCrashPage("dumb://home/crash?url=https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1")) | ||
| 19 | - | assert.Equal(t, "", extractOriginalURIFromCrashPage("dumb://home/crash")) | 19 | + | assert.Empty(t, extractOriginalURIFromCrashPage("dumb://home/crash")) |
| 20 | assert.Equal(t, "https://example.com", extractOriginalURIFromCrashPage("https://example.com")) | 20 | assert.Equal(t, "https://example.com", extractOriginalURIFromCrashPage("https://example.com")) | ||
| 21 | assert.Equal(t, "%%%bad", extractOriginalURIFromCrashPage("%%%bad")) | 21 | assert.Equal(t, "%%%bad", extractOriginalURIFromCrashPage("%%%bad")) | ||
| 22 | } | 22 | } | ||
M internal/ui/coordinator/oauth.go +2 -2
| @@ -141,7 +141,7 @@ func IsOAuthError(url string) bool { | |||
| 141 | 141 | return strings.Contains(lower, "error=") | |
| 142 | 142 | } | |
| 143 | 143 | ||
| 144 | - | func composeOnClose(existing func(), next func()) func() { | |
| 144 | + | func composeOnClose(existing, next func()) func() { | |
| 145 | 145 | if existing == nil { | |
| 146 | 146 | return next | |
| 147 | 147 | } | |
| @@ -154,7 +154,7 @@ func composeOnClose(existing func(), next func()) func() { | |||
| 154 | 154 | } | |
| 155 | 155 | } | |
| 156 | 156 | ||
| 157 | - | func composeOnURIChanged(existing func(string), next func(string)) func(string) { | |
| 157 | + | func composeOnURIChanged(existing, next func(string)) func(string) { | |
| 158 | 158 | if existing == nil { | |
| 159 | 159 | return next | |
| 160 | 160 | } | |
| @@ -141,7 +141,7 @@ func IsOAuthError(url string) bool { | |||||
| 141 | return strings.Contains(lower, "error=") | 141 | return strings.Contains(lower, "error=") | ||
| 142 | } | 142 | } | ||
| 143 | 143 | ||||
| 144 | - | func composeOnClose(existing func(), next func()) func() { | 144 | + | func composeOnClose(existing, next func()) func() { |
| 145 | if existing == nil { | 145 | if existing == nil { | ||
| 146 | return next | 146 | return next | ||
| 147 | } | 147 | } | ||
| @@ -154,7 +154,7 @@ func composeOnClose(existing func(), next func()) func() { | |||||
| 154 | } | 154 | } | ||
| 155 | } | 155 | } | ||
| 156 | 156 | ||||
| 157 | - | func composeOnURIChanged(existing func(string), next func(string)) func(string) { | 157 | + | func composeOnURIChanged(existing, next func(string)) func(string) { |
| 158 | if existing == nil { | 158 | if existing == nil { | ||
| 159 | return next | 159 | return next | ||
| 160 | } | 160 | } | ||