feat(filtering): extend content filtering service with additional rule handling
Add support for extended filtering operations, codec improvements for data serialization, and browser service enhancements for content filtering integration.
- commit
- b6ebf7a6d345c527b87082bfbe1aeb583c67fab8
- author
- bnema · · 2025-12-06 20:52 UTC
- parent
- 81a156d
- signature
- unsigned
3 files · +65 -7
M internal/app/environment/codec.go +9 -0
| @@ -73,6 +73,15 @@ func ApplyCodecConfiguration(codecPrefs config.CodecConfig) { | |||
| 73 | 73 | logging.Info(fmt.Sprintf("[codec] Blocked VP8 decoders")) | |
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | + | // Promote hardware JPEG decoders for faster image loading | |
| 77 | + | jpegRanks := []string{ | |
| 78 | + | "vaapijpegdec:512", // VA-API JPEG decoder (highest priority) | |
| 79 | + | "nvjpegdec:384", // NVDEC JPEG decoder | |
| 80 | + | "jpegdec:256", // Software fallback | |
| 81 | + | } | |
| 82 | + | rankSettings = append(rankSettings, jpegRanks...) | |
| 83 | + | logging.Info("[codec] Promoted hardware JPEG decoders") | |
| 84 | + | ||
| 76 | 85 | // Apply the complete ranking system | |
| 77 | 86 | if len(rankSettings) > 0 { | |
| 78 | 87 | finalRank := strings.Join(rankSettings, ",") | |
| @@ -73,6 +73,15 @@ func ApplyCodecConfiguration(codecPrefs config.CodecConfig) { | |||||
| 73 | logging.Info(fmt.Sprintf("[codec] Blocked VP8 decoders")) | 73 | logging.Info(fmt.Sprintf("[codec] Blocked VP8 decoders")) | ||
| 74 | } | 74 | } | ||
| 75 | 75 | ||||
| 76 | + | // Promote hardware JPEG decoders for faster image loading | |||
| 77 | + | jpegRanks := []string{ | |||
| 78 | + | "vaapijpegdec:512", // VA-API JPEG decoder (highest priority) | |||
| 79 | + | "nvjpegdec:384", // NVDEC JPEG decoder | |||
| 80 | + | "jpegdec:256", // Software fallback | |||
| 81 | + | } | |||
| 82 | + | rankSettings = append(rankSettings, jpegRanks...) | |||
| 83 | + | logging.Info("[codec] Promoted hardware JPEG decoders") | |||
| 84 | + | ||||
| 76 | // Apply the complete ranking system | 85 | // Apply the complete ranking system | ||
| 77 | if len(rankSettings) > 0 { | 86 | if len(rankSettings) > 0 { | ||
| 78 | finalRank := strings.Join(rankSettings, ",") | 87 | finalRank := strings.Join(rankSettings, ",") | ||
M internal/filtering/service.go +33 -7
| @@ -80,6 +80,9 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) { | |||
| 80 | 80 | for _, wv := range webViews { | |
| 81 | 81 | webView := wv // capture for closure | |
| 82 | 82 | pkgwebkit.RunOnMainThread(func() { | |
| 83 | + | if webView.IsDestroyed() { | |
| 84 | + | return | |
| 85 | + | } | |
| 83 | 86 | s.injectCosmeticBaseScript(webView) | |
| 84 | 87 | if uri := webView.GetCurrentURL(); uri != "" { | |
| 85 | 88 | s.injectCosmeticRules(webView, uri) | |
| @@ -115,6 +118,9 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) { | |||
| 115 | 118 | for _, wv := range currentWebViews { | |
| 116 | 119 | webView := wv // capture for closure | |
| 117 | 120 | pkgwebkit.RunOnMainThread(func() { | |
| 121 | + | if webView.IsDestroyed() { | |
| 122 | + | return | |
| 123 | + | } | |
| 118 | 124 | s.applyFiltersToWebView(webView) | |
| 119 | 125 | }) | |
| 120 | 126 | } | |
| @@ -150,8 +156,12 @@ func (s *ContentBlockingService) RegisterWebView(wv *pkgwebkit.WebView) { | |||
| 150 | 156 | ||
| 151 | 157 | // Apply network filters if already compiled (instant) | |
| 152 | 158 | if s.contentBlockingMgr.IsFilterCompiled() { | |
| 159 | + | webView := wv // capture for closure | |
| 153 | 160 | pkgwebkit.RunOnMainThread(func() { | |
| 154 | - | s.applyFiltersToWebView(wv) | |
| 161 | + | if webView.IsDestroyed() { | |
| 162 | + | return | |
| 163 | + | } | |
| 164 | + | s.applyFiltersToWebView(webView) | |
| 155 | 165 | }) | |
| 156 | 166 | } | |
| 157 | 167 | } | |
| @@ -159,7 +169,7 @@ func (s *ContentBlockingService) RegisterWebView(wv *pkgwebkit.WebView) { | |||
| 159 | 169 | // injectCosmeticBaseScript injects the base cosmetic filtering script into a WebView. | |
| 160 | 170 | // This script sets up the cosmetic filtering infrastructure (MutationObserver, hide functions, etc.) | |
| 161 | 171 | func (s *ContentBlockingService) injectCosmeticBaseScript(wv *pkgwebkit.WebView) { | |
| 162 | - | if wv == nil { | |
| 172 | + | if wv == nil || wv.IsDestroyed() { | |
| 163 | 173 | return | |
| 164 | 174 | } | |
| 165 | 175 | ||
| @@ -217,6 +227,10 @@ func (s *ContentBlockingService) UnregisterWebView(wv *pkgwebkit.WebView) { | |||
| 217 | 227 | ||
| 218 | 228 | // applyFiltersToWebView applies both network and cosmetic filters to a WebView. | |
| 219 | 229 | func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) { | |
| 230 | + | if wv == nil || wv.IsDestroyed() { | |
| 231 | + | return | |
| 232 | + | } | |
| 233 | + | ||
| 220 | 234 | s.mu.RLock() | |
| 221 | 235 | networkJSON := s.networkFilterJSON | |
| 222 | 236 | state := s.webViews[wv] | |
| @@ -224,15 +238,19 @@ func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) { | |||
| 224 | 238 | ||
| 225 | 239 | // Apply network filters if not already applied | |
| 226 | 240 | if !state.networkApplied && len(networkJSON) > 0 { | |
| 241 | + | webView := wv // capture for closure | |
| 227 | 242 | pkgwebkit.RunOnMainThread(func() { | |
| 228 | - | s.applyNetworkFilters(wv, networkJSON) | |
| 243 | + | if webView.IsDestroyed() { | |
| 244 | + | return | |
| 245 | + | } | |
| 246 | + | s.applyNetworkFilters(webView, networkJSON) | |
| 229 | 247 | }) | |
| 230 | 248 | } | |
| 231 | 249 | } | |
| 232 | 250 | ||
| 233 | 251 | // applyNetworkFilters applies network blocking rules to a WebView. | |
| 234 | 252 | func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filterJSON []byte) { | |
| 235 | - | if wv == nil { | |
| 253 | + | if wv == nil || wv.IsDestroyed() { | |
| 236 | 254 | return | |
| 237 | 255 | } | |
| 238 | 256 | ||
| @@ -284,14 +302,18 @@ func (s *ContentBlockingService) setupCosmeticHooks(wv *pkgwebkit.WebView) { | |||
| 284 | 302 | } | |
| 285 | 303 | ||
| 286 | 304 | // Register load-committed handler to inject domain-specific cosmetic rules | |
| 305 | + | webView := wv // capture for closure | |
| 287 | 306 | wv.RegisterLoadCommittedHandler(func(uri string) { | |
| 288 | - | s.injectCosmeticRules(wv, uri) | |
| 307 | + | if webView.IsDestroyed() { | |
| 308 | + | return | |
| 309 | + | } | |
| 310 | + | s.injectCosmeticRules(webView, uri) | |
| 289 | 311 | }) | |
| 290 | 312 | } | |
| 291 | 313 | ||
| 292 | 314 | // injectCosmeticRules injects cosmetic filtering rules for the current page. | |
| 293 | 315 | func (s *ContentBlockingService) injectCosmeticRules(wv *pkgwebkit.WebView, uri string) { | |
| 294 | - | if wv == nil || uri == "" { | |
| 316 | + | if wv == nil || wv.IsDestroyed() || uri == "" { | |
| 295 | 317 | return | |
| 296 | 318 | } | |
| 297 | 319 | ||
| @@ -319,8 +341,12 @@ func (s *ContentBlockingService) injectCosmeticRules(wv *pkgwebkit.WebView, uri | |||
| 319 | 341 | ||
| 320 | 342 | // Inject the script | |
| 321 | 343 | logging.Debug(fmt.Sprintf("[filtering] Injecting cosmetic rules for domain: %s (%d bytes)", domain, len(script))) | |
| 344 | + | webView := wv // capture for closure | |
| 322 | 345 | pkgwebkit.RunOnMainThread(func() { | |
| 323 | - | if err := wv.InjectScript(script); err != nil { | |
| 346 | + | if webView.IsDestroyed() { | |
| 347 | + | return | |
| 348 | + | } | |
| 349 | + | if err := webView.InjectScript(script); err != nil { | |
| 324 | 350 | logging.Error(fmt.Sprintf("[filtering] Failed to inject cosmetic rules for %s: %v", domain, err)) | |
| 325 | 351 | } else { | |
| 326 | 352 | logging.Debug(fmt.Sprintf("[filtering] Successfully injected cosmetic rules for domain: %s", domain)) | |
| @@ -80,6 +80,9 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) { | |||||
| 80 | for _, wv := range webViews { | 80 | for _, wv := range webViews { | ||
| 81 | webView := wv // capture for closure | 81 | webView := wv // capture for closure | ||
| 82 | pkgwebkit.RunOnMainThread(func() { | 82 | pkgwebkit.RunOnMainThread(func() { | ||
| 83 | + | if webView.IsDestroyed() { | |||
| 84 | + | return | |||
| 85 | + | } | |||
| 83 | s.injectCosmeticBaseScript(webView) | 86 | s.injectCosmeticBaseScript(webView) | ||
| 84 | if uri := webView.GetCurrentURL(); uri != "" { | 87 | if uri := webView.GetCurrentURL(); uri != "" { | ||
| 85 | s.injectCosmeticRules(webView, uri) | 88 | s.injectCosmeticRules(webView, uri) | ||
| @@ -115,6 +118,9 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) { | |||||
| 115 | for _, wv := range currentWebViews { | 118 | for _, wv := range currentWebViews { | ||
| 116 | webView := wv // capture for closure | 119 | webView := wv // capture for closure | ||
| 117 | pkgwebkit.RunOnMainThread(func() { | 120 | pkgwebkit.RunOnMainThread(func() { | ||
| 121 | + | if webView.IsDestroyed() { | |||
| 122 | + | return | |||
| 123 | + | } | |||
| 118 | s.applyFiltersToWebView(webView) | 124 | s.applyFiltersToWebView(webView) | ||
| 119 | }) | 125 | }) | ||
| 120 | } | 126 | } | ||
| @@ -150,8 +156,12 @@ func (s *ContentBlockingService) RegisterWebView(wv *pkgwebkit.WebView) { | |||||
| 150 | 156 | ||||
| 151 | // Apply network filters if already compiled (instant) | 157 | // Apply network filters if already compiled (instant) | ||
| 152 | if s.contentBlockingMgr.IsFilterCompiled() { | 158 | if s.contentBlockingMgr.IsFilterCompiled() { | ||
| 159 | + | webView := wv // capture for closure | |||
| 153 | pkgwebkit.RunOnMainThread(func() { | 160 | pkgwebkit.RunOnMainThread(func() { | ||
| 154 | - | s.applyFiltersToWebView(wv) | 161 | + | if webView.IsDestroyed() { |
| 162 | + | return | |||
| 163 | + | } | |||
| 164 | + | s.applyFiltersToWebView(webView) | |||
| 155 | }) | 165 | }) | ||
| 156 | } | 166 | } | ||
| 157 | } | 167 | } | ||
| @@ -159,7 +169,7 @@ func (s *ContentBlockingService) RegisterWebView(wv *pkgwebkit.WebView) { | |||||
| 159 | // injectCosmeticBaseScript injects the base cosmetic filtering script into a WebView. | 169 | // injectCosmeticBaseScript injects the base cosmetic filtering script into a WebView. | ||
| 160 | // This script sets up the cosmetic filtering infrastructure (MutationObserver, hide functions, etc.) | 170 | // This script sets up the cosmetic filtering infrastructure (MutationObserver, hide functions, etc.) | ||
| 161 | func (s *ContentBlockingService) injectCosmeticBaseScript(wv *pkgwebkit.WebView) { | 171 | func (s *ContentBlockingService) injectCosmeticBaseScript(wv *pkgwebkit.WebView) { | ||
| 162 | - | if wv == nil { | 172 | + | if wv == nil || wv.IsDestroyed() { |
| 163 | return | 173 | return | ||
| 164 | } | 174 | } | ||
| 165 | 175 | ||||
| @@ -217,6 +227,10 @@ func (s *ContentBlockingService) UnregisterWebView(wv *pkgwebkit.WebView) { | |||||
| 217 | 227 | ||||
| 218 | // applyFiltersToWebView applies both network and cosmetic filters to a WebView. | 228 | // applyFiltersToWebView applies both network and cosmetic filters to a WebView. | ||
| 219 | func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) { | 229 | func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) { | ||
| 230 | + | if wv == nil || wv.IsDestroyed() { | |||
| 231 | + | return | |||
| 232 | + | } | |||
| 233 | + | ||||
| 220 | s.mu.RLock() | 234 | s.mu.RLock() | ||
| 221 | networkJSON := s.networkFilterJSON | 235 | networkJSON := s.networkFilterJSON | ||
| 222 | state := s.webViews[wv] | 236 | state := s.webViews[wv] | ||
| @@ -224,15 +238,19 @@ func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) { | |||||
| 224 | 238 | ||||
| 225 | // Apply network filters if not already applied | 239 | // Apply network filters if not already applied | ||
| 226 | if !state.networkApplied && len(networkJSON) > 0 { | 240 | if !state.networkApplied && len(networkJSON) > 0 { | ||
| 241 | + | webView := wv // capture for closure | |||
| 227 | pkgwebkit.RunOnMainThread(func() { | 242 | pkgwebkit.RunOnMainThread(func() { | ||
| 228 | - | s.applyNetworkFilters(wv, networkJSON) | 243 | + | if webView.IsDestroyed() { |
| 244 | + | return | |||
| 245 | + | } | |||
| 246 | + | s.applyNetworkFilters(webView, networkJSON) | |||
| 229 | }) | 247 | }) | ||
| 230 | } | 248 | } | ||
| 231 | } | 249 | } | ||
| 232 | 250 | ||||
| 233 | // applyNetworkFilters applies network blocking rules to a WebView. | 251 | // applyNetworkFilters applies network blocking rules to a WebView. | ||
| 234 | func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filterJSON []byte) { | 252 | func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filterJSON []byte) { | ||
| 235 | - | if wv == nil { | 253 | + | if wv == nil || wv.IsDestroyed() { |
| 236 | return | 254 | return | ||
| 237 | } | 255 | } | ||
| 238 | 256 | ||||
| @@ -284,14 +302,18 @@ func (s *ContentBlockingService) setupCosmeticHooks(wv *pkgwebkit.WebView) { | |||||
| 284 | } | 302 | } | ||
| 285 | 303 | ||||
| 286 | // Register load-committed handler to inject domain-specific cosmetic rules | 304 | // Register load-committed handler to inject domain-specific cosmetic rules | ||
| 305 | + | webView := wv // capture for closure | |||
| 287 | wv.RegisterLoadCommittedHandler(func(uri string) { | 306 | wv.RegisterLoadCommittedHandler(func(uri string) { | ||
| 288 | - | s.injectCosmeticRules(wv, uri) | 307 | + | if webView.IsDestroyed() { |
| 308 | + | return | |||
| 309 | + | } | |||
| 310 | + | s.injectCosmeticRules(webView, uri) | |||
| 289 | }) | 311 | }) | ||
| 290 | } | 312 | } | ||
| 291 | 313 | ||||
| 292 | // injectCosmeticRules injects cosmetic filtering rules for the current page. | 314 | // injectCosmeticRules injects cosmetic filtering rules for the current page. | ||
| 293 | func (s *ContentBlockingService) injectCosmeticRules(wv *pkgwebkit.WebView, uri string) { | 315 | func (s *ContentBlockingService) injectCosmeticRules(wv *pkgwebkit.WebView, uri string) { | ||
| 294 | - | if wv == nil || uri == "" { | 316 | + | if wv == nil || wv.IsDestroyed() || uri == "" { |
| 295 | return | 317 | return | ||
| 296 | } | 318 | } | ||
| 297 | 319 | ||||
| @@ -319,8 +341,12 @@ func (s *ContentBlockingService) injectCosmeticRules(wv *pkgwebkit.WebView, uri | |||||
| 319 | 341 | ||||
| 320 | // Inject the script | 342 | // Inject the script | ||
| 321 | logging.Debug(fmt.Sprintf("[filtering] Injecting cosmetic rules for domain: %s (%d bytes)", domain, len(script))) | 343 | logging.Debug(fmt.Sprintf("[filtering] Injecting cosmetic rules for domain: %s (%d bytes)", domain, len(script))) | ||
| 344 | + | webView := wv // capture for closure | |||
| 322 | pkgwebkit.RunOnMainThread(func() { | 345 | pkgwebkit.RunOnMainThread(func() { | ||
| 323 | - | if err := wv.InjectScript(script); err != nil { | 346 | + | if webView.IsDestroyed() { |
| 347 | + | return | |||
| 348 | + | } | |||
| 349 | + | if err := webView.InjectScript(script); err != nil { | |||
| 324 | logging.Error(fmt.Sprintf("[filtering] Failed to inject cosmetic rules for %s: %v", domain, err)) | 350 | logging.Error(fmt.Sprintf("[filtering] Failed to inject cosmetic rules for %s: %v", domain, err)) | ||
| 325 | } else { | 351 | } else { | ||
| 326 | logging.Debug(fmt.Sprintf("[filtering] Successfully injected cosmetic rules for domain: %s", domain)) | 352 | logging.Debug(fmt.Sprintf("[filtering] Successfully injected cosmetic rules for domain: %s", domain)) | ||
M internal/services/browser.go +23 -0
| @@ -1227,9 +1227,17 @@ type HistoryAnalytics struct { | |||
| 1227 | 1227 | TotalVisits float64 `json:"total_visits"` | |
| 1228 | 1228 | UniqueDays int64 `json:"unique_days"` | |
| 1229 | 1229 | TopDomains []DomainStat `json:"top_domains"` | |
| 1230 | + | DailyVisits []DailyVisitCount `json:"daily_visits"` | |
| 1230 | 1231 | HourlyDist []HourlyVisitCount `json:"hourly_distribution"` | |
| 1231 | 1232 | } | |
| 1232 | 1233 | ||
| 1234 | + | // DailyVisitCount represents visit counts by day. | |
| 1235 | + | type DailyVisitCount struct { | |
| 1236 | + | Day string `json:"day"` | |
| 1237 | + | Entries int64 `json:"entries"` | |
| 1238 | + | Visits float64 `json:"visits"` | |
| 1239 | + | } | |
| 1240 | + | ||
| 1233 | 1241 | // HourlyVisitCount represents visit counts by hour. | |
| 1234 | 1242 | type HourlyVisitCount struct { | |
| 1235 | 1243 | Hour int `json:"hour"` | |
| @@ -1248,6 +1256,20 @@ func (s *BrowserService) GetHistoryAnalytics(ctx context.Context) (*HistoryAnaly | |||
| 1248 | 1256 | topDomains = []DomainStat{} | |
| 1249 | 1257 | } | |
| 1250 | 1258 | ||
| 1259 | + | // Get daily visit counts for last 30 days | |
| 1260 | + | dailyRows, err := s.dbQueries.GetDailyVisitCount(ctx, "-30 days") | |
| 1261 | + | if err != nil { | |
| 1262 | + | dailyRows = []db.GetDailyVisitCountRow{} | |
| 1263 | + | } | |
| 1264 | + | dailyVisits := make([]DailyVisitCount, len(dailyRows)) | |
| 1265 | + | for i, d := range dailyRows { | |
| 1266 | + | dailyVisits[i] = DailyVisitCount{ | |
| 1267 | + | Day: d.Day.(string), | |
| 1268 | + | Entries: d.Entries, | |
| 1269 | + | Visits: d.Visits.Float64, | |
| 1270 | + | } | |
| 1271 | + | } | |
| 1272 | + | ||
| 1251 | 1273 | hourlyDist, err := s.dbQueries.GetHourlyDistribution(ctx) | |
| 1252 | 1274 | if err != nil { | |
| 1253 | 1275 | hourlyDist = []db.GetHourlyDistributionRow{} | |
| @@ -1266,6 +1288,7 @@ func (s *BrowserService) GetHistoryAnalytics(ctx context.Context) (*HistoryAnaly | |||
| 1266 | 1288 | TotalVisits: stats.TotalVisits.Float64, | |
| 1267 | 1289 | UniqueDays: stats.UniqueDays, | |
| 1268 | 1290 | TopDomains: topDomains, | |
| 1291 | + | DailyVisits: dailyVisits, | |
| 1269 | 1292 | HourlyDist: hourly, | |
| 1270 | 1293 | }, nil | |
| 1271 | 1294 | } | |
| @@ -1227,9 +1227,17 @@ type HistoryAnalytics struct { | |||||
| 1227 | TotalVisits float64 `json:"total_visits"` | 1227 | TotalVisits float64 `json:"total_visits"` | ||
| 1228 | UniqueDays int64 `json:"unique_days"` | 1228 | UniqueDays int64 `json:"unique_days"` | ||
| 1229 | TopDomains []DomainStat `json:"top_domains"` | 1229 | TopDomains []DomainStat `json:"top_domains"` | ||
| 1230 | + | DailyVisits []DailyVisitCount `json:"daily_visits"` | |||
| 1230 | HourlyDist []HourlyVisitCount `json:"hourly_distribution"` | 1231 | HourlyDist []HourlyVisitCount `json:"hourly_distribution"` | ||
| 1231 | } | 1232 | } | ||
| 1232 | 1233 | ||||
| 1234 | + | // DailyVisitCount represents visit counts by day. | |||
| 1235 | + | type DailyVisitCount struct { | |||
| 1236 | + | Day string `json:"day"` | |||
| 1237 | + | Entries int64 `json:"entries"` | |||
| 1238 | + | Visits float64 `json:"visits"` | |||
| 1239 | + | } | |||
| 1240 | + | ||||
| 1233 | // HourlyVisitCount represents visit counts by hour. | 1241 | // HourlyVisitCount represents visit counts by hour. | ||
| 1234 | type HourlyVisitCount struct { | 1242 | type HourlyVisitCount struct { | ||
| 1235 | Hour int `json:"hour"` | 1243 | Hour int `json:"hour"` | ||
| @@ -1248,6 +1256,20 @@ func (s *BrowserService) GetHistoryAnalytics(ctx context.Context) (*HistoryAnaly | |||||
| 1248 | topDomains = []DomainStat{} | 1256 | topDomains = []DomainStat{} | ||
| 1249 | } | 1257 | } | ||
| 1250 | 1258 | ||||
| 1259 | + | // Get daily visit counts for last 30 days | |||
| 1260 | + | dailyRows, err := s.dbQueries.GetDailyVisitCount(ctx, "-30 days") | |||
| 1261 | + | if err != nil { | |||
| 1262 | + | dailyRows = []db.GetDailyVisitCountRow{} | |||
| 1263 | + | } | |||
| 1264 | + | dailyVisits := make([]DailyVisitCount, len(dailyRows)) | |||
| 1265 | + | for i, d := range dailyRows { | |||
| 1266 | + | dailyVisits[i] = DailyVisitCount{ | |||
| 1267 | + | Day: d.Day.(string), | |||
| 1268 | + | Entries: d.Entries, | |||
| 1269 | + | Visits: d.Visits.Float64, | |||
| 1270 | + | } | |||
| 1271 | + | } | |||
| 1272 | + | ||||
| 1251 | hourlyDist, err := s.dbQueries.GetHourlyDistribution(ctx) | 1273 | hourlyDist, err := s.dbQueries.GetHourlyDistribution(ctx) | ||
| 1252 | if err != nil { | 1274 | if err != nil { | ||
| 1253 | hourlyDist = []db.GetHourlyDistributionRow{} | 1275 | hourlyDist = []db.GetHourlyDistributionRow{} | ||
| @@ -1266,6 +1288,7 @@ func (s *BrowserService) GetHistoryAnalytics(ctx context.Context) (*HistoryAnaly | |||||
| 1266 | TotalVisits: stats.TotalVisits.Float64, | 1288 | TotalVisits: stats.TotalVisits.Float64, | ||
| 1267 | UniqueDays: stats.UniqueDays, | 1289 | UniqueDays: stats.UniqueDays, | ||
| 1268 | TopDomains: topDomains, | 1290 | TopDomains: topDomains, | ||
| 1291 | + | DailyVisits: dailyVisits, | |||
| 1269 | HourlyDist: hourly, | 1292 | HourlyDist: hourly, | ||
| 1270 | }, nil | 1293 | }, nil | ||
| 1271 | } | 1294 | } | ||