v0.28.0 3cb3770

search_history.go RawHistoryFolder

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

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