detached d2e410e

feat(usecase): add analytics methods to search history use case

commit
d2e410eadea042593ac394fa7051d4274e209069
author
brice · · 2025-12-16 19:23 UTC
committer
brice · · 2025-12-16 18:39 UTC
parent
c5f1087
signature
unsigned
2 files · +118 -0
M internal/application/usecase/manage_favorites.go +54 -0
@@ -408,3 +408,57 @@ func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID
408408
409409 return uc.tagRepo.GetForFavorite(ctx, favID)
410410 }
411+
412+// UpdateFolder updates a folder's name and icon.
413+func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error {
414+ log := logging.FromContext(ctx)
415+ log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder")
416+
417+ folder, err := uc.folderRepo.FindByID(ctx, id)
418+ if err != nil {
419+ return fmt.Errorf("failed to find folder: %w", err)
420+ }
421+ if folder == nil {
422+ return fmt.Errorf("folder %d not found", id)
423+ }
424+
425+ if name != "" {
426+ folder.Name = name
427+ }
428+ folder.Icon = icon
429+
430+ if err := uc.folderRepo.Save(ctx, folder); err != nil {
431+ return fmt.Errorf("failed to update folder: %w", err)
432+ }
433+
434+ log.Info().Int64("id", int64(id)).Msg("folder updated")
435+ return nil
436+}
437+
438+// UpdateTag updates a tag's name and color.
439+func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error {
440+ log := logging.FromContext(ctx)
441+ log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag")
442+
443+ tag, err := uc.tagRepo.FindByID(ctx, id)
444+ if err != nil {
445+ return fmt.Errorf("failed to find tag: %w", err)
446+ }
447+ if tag == nil {
448+ return fmt.Errorf("tag %d not found", id)
449+ }
450+
451+ if name != "" {
452+ tag.Name = name
453+ }
454+ if color != "" {
455+ tag.Color = color
456+ }
457+
458+ if err := uc.tagRepo.Save(ctx, tag); err != nil {
459+ return fmt.Errorf("failed to update tag: %w", err)
460+ }
461+
462+ log.Info().Int64("id", int64(id)).Msg("tag updated")
463+ return nil
464+}
@@ -408,3 +408,57 @@ func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID
408 408
409 return uc.tagRepo.GetForFavorite(ctx, favID)409 return uc.tagRepo.GetForFavorite(ctx, favID)
410 }410 }
411+
412+// UpdateFolder updates a folder's name and icon.
413+func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error {
414+ log := logging.FromContext(ctx)
415+ log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder")
416+
417+ folder, err := uc.folderRepo.FindByID(ctx, id)
418+ if err != nil {
419+ return fmt.Errorf("failed to find folder: %w", err)
420+ }
421+ if folder == nil {
422+ return fmt.Errorf("folder %d not found", id)
423+ }
424+
425+ if name != "" {
426+ folder.Name = name
427+ }
428+ folder.Icon = icon
429+
430+ if err := uc.folderRepo.Save(ctx, folder); err != nil {
431+ return fmt.Errorf("failed to update folder: %w", err)
432+ }
433+
434+ log.Info().Int64("id", int64(id)).Msg("folder updated")
435+ return nil
436+}
437+
438+// UpdateTag updates a tag's name and color.
439+func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error {
440+ log := logging.FromContext(ctx)
441+ log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag")
442+
443+ tag, err := uc.tagRepo.FindByID(ctx, id)
444+ if err != nil {
445+ return fmt.Errorf("failed to find tag: %w", err)
446+ }
447+ if tag == nil {
448+ return fmt.Errorf("tag %d not found", id)
449+ }
450+
451+ if name != "" {
452+ tag.Name = name
453+ }
454+ if color != "" {
455+ tag.Color = color
456+ }
457+
458+ if err := uc.tagRepo.Save(ctx, tag); err != nil {
459+ return fmt.Errorf("failed to update tag: %w", err)
460+ }
461+
462+ log.Info().Int64("id", int64(id)).Msg("tag updated")
463+ return nil
464+}
M internal/application/usecase/search_history.go +64 -0
@@ -165,3 +165,67 @@ func (uc *SearchHistoryUseCase) Delete(ctx context.Context, id int64) error {
165165 log.Debug().Int64("id", id).Msg("history entry deleted")
166166 return nil
167167 }
168+
169+// DeleteByDomain removes all history entries for a domain.
170+func (uc *SearchHistoryUseCase) DeleteByDomain(ctx context.Context, domain string) error {
171+ log := logging.FromContext(ctx)
172+ log.Debug().Str("domain", domain).Msg("deleting history by domain")
173+
174+ if err := uc.historyRepo.DeleteByDomain(ctx, domain); err != nil {
175+ return fmt.Errorf("failed to delete history by domain: %w", err)
176+ }
177+
178+ log.Info().Str("domain", domain).Msg("history deleted for domain")
179+ return nil
180+}
181+
182+// GetDomainStats retrieves per-domain visit statistics.
183+func (uc *SearchHistoryUseCase) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
184+ log := logging.FromContext(ctx)
185+ log.Debug().Int("limit", limit).Msg("getting domain stats")
186+
187+ if limit <= 0 {
188+ limit = 20
189+ }
190+
191+ return uc.historyRepo.GetDomainStats(ctx, limit)
192+}
193+
194+// GetAnalytics retrieves aggregated history analytics for the homepage.
195+func (uc *SearchHistoryUseCase) GetAnalytics(ctx context.Context) (*entity.HistoryAnalytics, error) {
196+ log := logging.FromContext(ctx)
197+ log.Debug().Msg("getting history analytics")
198+
199+ // Get basic stats
200+ stats, err := uc.historyRepo.GetStats(ctx)
201+ if err != nil {
202+ return nil, fmt.Errorf("failed to get history stats: %w", err)
203+ }
204+
205+ // Get top domains
206+ domains, err := uc.historyRepo.GetDomainStats(ctx, 10)
207+ if err != nil {
208+ return nil, fmt.Errorf("failed to get domain stats: %w", err)
209+ }
210+
211+ // Get daily visits for last 30 days
212+ dailyVisits, err := uc.historyRepo.GetDailyVisitCount(ctx, "-30 days")
213+ if err != nil {
214+ return nil, fmt.Errorf("failed to get daily visits: %w", err)
215+ }
216+
217+ // Get hourly distribution
218+ hourlyDist, err := uc.historyRepo.GetHourlyDistribution(ctx)
219+ if err != nil {
220+ return nil, fmt.Errorf("failed to get hourly distribution: %w", err)
221+ }
222+
223+ return &entity.HistoryAnalytics{
224+ TotalEntries: stats.TotalEntries,
225+ TotalVisits: stats.TotalVisits,
226+ UniqueDays: stats.UniqueDays,
227+ TopDomains: domains,
228+ DailyVisits: dailyVisits,
229+ HourlyDistribution: hourlyDist,
230+ }, nil
231+}
@@ -165,3 +165,67 @@ func (uc *SearchHistoryUseCase) Delete(ctx context.Context, id int64) error {
165 log.Debug().Int64("id", id).Msg("history entry deleted")165 log.Debug().Int64("id", id).Msg("history entry deleted")
166 return nil166 return nil
167 }167 }
168+
169+// DeleteByDomain removes all history entries for a domain.
170+func (uc *SearchHistoryUseCase) DeleteByDomain(ctx context.Context, domain string) error {
171+ log := logging.FromContext(ctx)
172+ log.Debug().Str("domain", domain).Msg("deleting history by domain")
173+
174+ if err := uc.historyRepo.DeleteByDomain(ctx, domain); err != nil {
175+ return fmt.Errorf("failed to delete history by domain: %w", err)
176+ }
177+
178+ log.Info().Str("domain", domain).Msg("history deleted for domain")
179+ return nil
180+}
181+
182+// GetDomainStats retrieves per-domain visit statistics.
183+func (uc *SearchHistoryUseCase) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
184+ log := logging.FromContext(ctx)
185+ log.Debug().Int("limit", limit).Msg("getting domain stats")
186+
187+ if limit <= 0 {
188+ limit = 20
189+ }
190+
191+ return uc.historyRepo.GetDomainStats(ctx, limit)
192+}
193+
194+// GetAnalytics retrieves aggregated history analytics for the homepage.
195+func (uc *SearchHistoryUseCase) GetAnalytics(ctx context.Context) (*entity.HistoryAnalytics, error) {
196+ log := logging.FromContext(ctx)
197+ log.Debug().Msg("getting history analytics")
198+
199+ // Get basic stats
200+ stats, err := uc.historyRepo.GetStats(ctx)
201+ if err != nil {
202+ return nil, fmt.Errorf("failed to get history stats: %w", err)
203+ }
204+
205+ // Get top domains
206+ domains, err := uc.historyRepo.GetDomainStats(ctx, 10)
207+ if err != nil {
208+ return nil, fmt.Errorf("failed to get domain stats: %w", err)
209+ }
210+
211+ // Get daily visits for last 30 days
212+ dailyVisits, err := uc.historyRepo.GetDailyVisitCount(ctx, "-30 days")
213+ if err != nil {
214+ return nil, fmt.Errorf("failed to get daily visits: %w", err)
215+ }
216+
217+ // Get hourly distribution
218+ hourlyDist, err := uc.historyRepo.GetHourlyDistribution(ctx)
219+ if err != nil {
220+ return nil, fmt.Errorf("failed to get hourly distribution: %w", err)
221+ }
222+
223+ return &entity.HistoryAnalytics{
224+ TotalEntries: stats.TotalEntries,
225+ TotalVisits: stats.TotalVisits,
226+ UniqueDays: stats.UniqueDays,
227+ TopDomains: domains,
228+ DailyVisits: dailyVisits,
229+ HourlyDistribution: hourlyDist,
230+ }, nil
231+}

Keyboard shortcuts

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

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

Branches 8

Tags 42

↑↓ moveenter openesc close