v0.23.2 bfd5eb4

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// GetRecentSince retrieves history entries visited within the last N days.
79// If days is 0, returns all history entries.
80// If days is negative, defaults to 30 days.
81func (uc *SearchHistoryUseCase) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
82	if days < 0 {
83		days = 30 // Default to 30 days for negative values
84	}
85
86	var entries []*entity.HistoryEntry
87	var err error
88
89	switch days {
90	case 0:
91		entries, err = uc.historyRepo.GetAllRecentHistory(ctx)
92	default:
93		entries, err = uc.historyRepo.GetRecentSince(ctx, days)
94	}
95
96	if err != nil {
97		return nil, fmt.Errorf("failed to get recent history: %w", err)
98	}
99
100	return entries, nil
101}
102
103// GetMostVisited retrieves history entries sorted by visit count within the last N days.
104// If days is 0, returns all history entries sorted by visit count.
105// If days is negative, defaults to 30 days.
106func (uc *SearchHistoryUseCase) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
107	if days < 0 {
108		days = 30 // Default to 30 days for negative values
109	}
110
111	var entries []*entity.HistoryEntry
112	var err error
113
114	switch days {
115	case 0:
116		entries, err = uc.historyRepo.GetAllMostVisited(ctx)
117	default:
118		entries, err = uc.historyRepo.GetMostVisited(ctx, days)
119	}
120
121	if err != nil {
122		return nil, fmt.Errorf("failed to get most visited history: %w", err)
123	}
124
125	return entries, nil
126}
127
128// FindByURL retrieves a history entry by its URL.
129func (uc *SearchHistoryUseCase) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) {
130	log := logging.FromContext(ctx)
131	log.Debug().Str("url", url).Msg("finding history entry by URL")
132
133	entry, err := uc.historyRepo.FindByURL(ctx, url)
134	if err != nil {
135		return nil, fmt.Errorf("failed to find history entry: %w", err)
136	}
137
138	if entry == nil {
139		log.Debug().Str("url", url).Msg("history entry not found")
140	}
141
142	return entry, nil
143}
144
145// ClearOlderThan deletes history entries older than the specified time.
146func (uc *SearchHistoryUseCase) ClearOlderThan(ctx context.Context, before time.Time) error {
147	log := logging.FromContext(ctx)
148	log.Debug().Time("before", before).Msg("clearing old history")
149
150	if err := uc.historyRepo.DeleteOlderThan(ctx, before); err != nil {
151		return fmt.Errorf("failed to clear history: %w", err)
152	}
153
154	log.Info().Time("before", before).Msg("old history cleared")
155	return nil
156}
157
158// ClearAll deletes all history entries.
159func (uc *SearchHistoryUseCase) ClearAll(ctx context.Context) error {
160	log := logging.FromContext(ctx)
161	log.Debug().Msg("clearing all history")
162
163	if err := uc.historyRepo.DeleteAll(ctx); err != nil {
164		return fmt.Errorf("failed to clear all history: %w", err)
165	}
166
167	log.Info().Msg("all history cleared")
168	return nil
169}
170
171// Delete removes a single history entry by ID.
172func (uc *SearchHistoryUseCase) Delete(ctx context.Context, id int64) error {
173	log := logging.FromContext(ctx)
174	log.Debug().Int64("id", id).Msg("deleting history entry")
175
176	if err := uc.historyRepo.Delete(ctx, id); err != nil {
177		return fmt.Errorf("failed to delete history entry: %w", err)
178	}
179
180	log.Debug().Int64("id", id).Msg("history entry deleted")
181	return nil
182}
183
184// DeleteByDomain removes all history entries for a domain.
185func (uc *SearchHistoryUseCase) DeleteByDomain(ctx context.Context, domain string) error {
186	log := logging.FromContext(ctx)
187	log.Debug().Str("domain", domain).Msg("deleting history by domain")
188
189	if err := uc.historyRepo.DeleteByDomain(ctx, domain); err != nil {
190		return fmt.Errorf("failed to delete history by domain: %w", err)
191	}
192
193	log.Info().Str("domain", domain).Msg("history deleted for domain")
194	return nil
195}
196
197// GetDomainStats retrieves per-domain visit statistics.
198func (uc *SearchHistoryUseCase) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
199	log := logging.FromContext(ctx)
200	log.Debug().Int("limit", limit).Msg("getting domain stats")
201
202	if limit <= 0 {
203		limit = 20
204	}
205
206	return uc.historyRepo.GetDomainStats(ctx, limit)
207}
208
209// GetAnalytics retrieves aggregated history analytics for the homepage.
210func (uc *SearchHistoryUseCase) GetAnalytics(ctx context.Context) (*entity.HistoryAnalytics, error) {
211	log := logging.FromContext(ctx)
212	log.Debug().Msg("getting history analytics")
213
214	// Get basic stats
215	stats, err := uc.historyRepo.GetStats(ctx)
216	if err != nil {
217		return nil, fmt.Errorf("failed to get history stats: %w", err)
218	}
219
220	// Get top domains
221	domains, err := uc.historyRepo.GetDomainStats(ctx, 10)
222	if err != nil {
223		return nil, fmt.Errorf("failed to get domain stats: %w", err)
224	}
225
226	// Get daily visits for last 30 days
227	dailyVisits, err := uc.historyRepo.GetDailyVisitCount(ctx, "-30 days")
228	if err != nil {
229		return nil, fmt.Errorf("failed to get daily visits: %w", err)
230	}
231
232	// Get hourly distribution
233	hourlyDist, err := uc.historyRepo.GetHourlyDistribution(ctx)
234	if err != nil {
235		return nil, fmt.Errorf("failed to get hourly distribution: %w", err)
236	}
237
238	return &entity.HistoryAnalytics{
239		TotalEntries:       stats.TotalEntries,
240		TotalVisits:        stats.TotalVisits,
241		UniqueDays:         stats.UniqueDays,
242		TopDomains:         domains,
243		DailyVisits:        dailyVisits,
244		HourlyDistribution: hourlyDist,
245	}, nil
246}

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