v0.21.0 73bf644

search_history.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"fmt"
6	"time"
7
8	"github.com/bnema/dumber/internal/domain/entity"
9	"github.com/bnema/dumber/internal/domain/repository"
10	"github.com/bnema/dumber/internal/logging"
11)
12
13// SearchHistoryUseCase handles history search and retrieval operations.
14type SearchHistoryUseCase struct {
15	historyRepo repository.HistoryRepository
16}
17
18// NewSearchHistoryUseCase creates a new history search use case.
19func NewSearchHistoryUseCase(historyRepo repository.HistoryRepository) *SearchHistoryUseCase {
20	return &SearchHistoryUseCase{
21		historyRepo: historyRepo,
22	}
23}
24
25// SearchInput contains search parameters.
26type SearchInput struct {
27	Query string
28	Limit int
29}
30
31// SearchOutput contains search results.
32type SearchOutput struct {
33	Matches []entity.HistoryMatch
34}
35
36// Search performs a full-text search on history entries using SQLite FTS5.
37// Returns only entries that actually match the query terms.
38func (uc *SearchHistoryUseCase) Search(ctx context.Context, input SearchInput) (*SearchOutput, error) {
39	log := logging.FromContext(ctx)
40
41	if input.Query == "" {
42		return &SearchOutput{Matches: []entity.HistoryMatch{}}, nil
43	}
44
45	limit := input.Limit
46	if limit <= 0 {
47		limit = 20 // Default limit
48	}
49
50	// Use repository's FTS5 search
51	matches, err := uc.historyRepo.Search(ctx, input.Query, limit)
52	if err != nil {
53		return nil, fmt.Errorf("failed to search history: %w", err)
54	}
55
56	log.Debug().
57		Str("query", input.Query).
58		Int("matches", len(matches)).
59		Msg("FTS5 search completed")
60
61	return &SearchOutput{Matches: matches}, nil
62}
63
64// GetRecent retrieves recent history entries with pagination.
65func (uc *SearchHistoryUseCase) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) {
66	if limit <= 0 {
67		limit = 50 // Default limit
68	}
69
70	entries, err := uc.historyRepo.GetRecent(ctx, limit, offset)
71	if err != nil {
72		return nil, fmt.Errorf("failed to get recent history: %w", err)
73	}
74
75	return entries, nil
76}
77
78// FindByURL retrieves a history entry by its URL.
79func (uc *SearchHistoryUseCase) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) {
80	log := logging.FromContext(ctx)
81	log.Debug().Str("url", url).Msg("finding history entry by URL")
82
83	entry, err := uc.historyRepo.FindByURL(ctx, url)
84	if err != nil {
85		return nil, fmt.Errorf("failed to find history entry: %w", err)
86	}
87
88	if entry == nil {
89		log.Debug().Str("url", url).Msg("history entry not found")
90	}
91
92	return entry, nil
93}
94
95// ClearOlderThan deletes history entries older than the specified time.
96func (uc *SearchHistoryUseCase) ClearOlderThan(ctx context.Context, before time.Time) error {
97	log := logging.FromContext(ctx)
98	log.Debug().Time("before", before).Msg("clearing old history")
99
100	if err := uc.historyRepo.DeleteOlderThan(ctx, before); err != nil {
101		return fmt.Errorf("failed to clear history: %w", err)
102	}
103
104	log.Info().Time("before", before).Msg("old history cleared")
105	return nil
106}
107
108// ClearAll deletes all history entries.
109func (uc *SearchHistoryUseCase) ClearAll(ctx context.Context) error {
110	log := logging.FromContext(ctx)
111	log.Debug().Msg("clearing all history")
112
113	if err := uc.historyRepo.DeleteAll(ctx); err != nil {
114		return fmt.Errorf("failed to clear all history: %w", err)
115	}
116
117	log.Info().Msg("all history cleared")
118	return nil
119}
120
121// Delete removes a single history entry by ID.
122func (uc *SearchHistoryUseCase) Delete(ctx context.Context, id int64) error {
123	log := logging.FromContext(ctx)
124	log.Debug().Int64("id", id).Msg("deleting history entry")
125
126	if err := uc.historyRepo.Delete(ctx, id); err != nil {
127		return fmt.Errorf("failed to delete history entry: %w", err)
128	}
129
130	log.Debug().Int64("id", id).Msg("history entry deleted")
131	return nil
132}
133
134// DeleteByDomain removes all history entries for a domain.
135func (uc *SearchHistoryUseCase) DeleteByDomain(ctx context.Context, domain string) error {
136	log := logging.FromContext(ctx)
137	log.Debug().Str("domain", domain).Msg("deleting history by domain")
138
139	if err := uc.historyRepo.DeleteByDomain(ctx, domain); err != nil {
140		return fmt.Errorf("failed to delete history by domain: %w", err)
141	}
142
143	log.Info().Str("domain", domain).Msg("history deleted for domain")
144	return nil
145}
146
147// GetDomainStats retrieves per-domain visit statistics.
148func (uc *SearchHistoryUseCase) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
149	log := logging.FromContext(ctx)
150	log.Debug().Int("limit", limit).Msg("getting domain stats")
151
152	if limit <= 0 {
153		limit = 20
154	}
155
156	return uc.historyRepo.GetDomainStats(ctx, limit)
157}
158
159// GetAnalytics retrieves aggregated history analytics for the homepage.
160func (uc *SearchHistoryUseCase) GetAnalytics(ctx context.Context) (*entity.HistoryAnalytics, error) {
161	log := logging.FromContext(ctx)
162	log.Debug().Msg("getting history analytics")
163
164	// Get basic stats
165	stats, err := uc.historyRepo.GetStats(ctx)
166	if err != nil {
167		return nil, fmt.Errorf("failed to get history stats: %w", err)
168	}
169
170	// Get top domains
171	domains, err := uc.historyRepo.GetDomainStats(ctx, 10)
172	if err != nil {
173		return nil, fmt.Errorf("failed to get domain stats: %w", err)
174	}
175
176	// Get daily visits for last 30 days
177	dailyVisits, err := uc.historyRepo.GetDailyVisitCount(ctx, "-30 days")
178	if err != nil {
179		return nil, fmt.Errorf("failed to get daily visits: %w", err)
180	}
181
182	// Get hourly distribution
183	hourlyDist, err := uc.historyRepo.GetHourlyDistribution(ctx)
184	if err != nil {
185		return nil, fmt.Errorf("failed to get hourly distribution: %w", err)
186	}
187
188	return &entity.HistoryAnalytics{
189		TotalEntries:       stats.TotalEntries,
190		TotalVisits:        stats.TotalVisits,
191		UniqueDays:         stats.UniqueDays,
192		TopDomains:         domains,
193		DailyVisits:        dailyVisits,
194		HourlyDistribution: hourlyDist,
195	}, nil
196}

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