1package homepage
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8 "time"
9
10 "github.com/bnema/dumber/internal/application/dto"
11 "github.com/bnema/dumber/internal/application/port"
12 "github.com/bnema/dumber/internal/domain/entity"
13 "github.com/bnema/dumber/internal/logging"
14)
15
16// HistoryHandlers handles history-related messages from the homepage.
17type HistoryHandlers struct {
18 historyUC port.HomepageHistory
19}
20
21// NewHistoryHandlers creates a new HistoryHandlers instance.
22func NewHistoryHandlers(historyUC port.HomepageHistory) *HistoryHandlers {
23 return &HistoryHandlers{historyUC: historyUC}
24}
25
26// timelineRequest is the payload for history_timeline messages.
27type timelineRequest struct {
28 RequestID string `json:"requestId"`
29 Limit int `json:"limit"`
30 Offset int `json:"offset"`
31}
32
33type timelineByDomainRequest struct {
34 RequestID string `json:"requestId"`
35 Domain string `json:"domain"`
36 Limit int `json:"limit"`
37 Offset int `json:"offset"`
38}
39
40type timelineWindowRequest struct {
41 RequestID string `json:"requestId"`
42 Before string `json:"before"`
43 BeforeID int64 `json:"beforeId"`
44 Domain string `json:"domain"`
45}
46
47// HandleTimeline handles history_timeline messages.
48func (h *HistoryHandlers) HandleTimeline() port.WebUIMessageHandler {
49 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
50 log := logging.FromContext(ctx)
51
52 var req timelineRequest
53 if err := json.Unmarshal(payload, &req); err != nil {
54 return NewErrorResponse("", err), nil
55 }
56
57 log.Debug().
58 Str("request_id", req.RequestID).
59 Int("limit", req.Limit).
60 Int("offset", req.Offset).
61 Msg("handling history_timeline")
62 if req.Limit <= 0 {
63 return NewErrorResponse(
64 req.RequestID,
65 fmt.Errorf("history_timeline requires a positive limit; use history_timeline_window for lazy history loading"),
66 ), nil
67 }
68
69 entries, err := h.historyUC.GetRecent(ctx, req.Limit, req.Offset)
70 if err != nil {
71 return NewErrorResponse(req.RequestID, err), nil
72 }
73
74 return NewSuccessResponse(req.RequestID, entries), nil
75 })
76}
77
78// HandleTimelineByDomain handles history_timeline_by_domain messages.
79func (h *HistoryHandlers) HandleTimelineByDomain() port.WebUIMessageHandler {
80 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
81 log := logging.FromContext(ctx)
82
83 var req timelineByDomainRequest
84 if err := json.Unmarshal(payload, &req); err != nil {
85 return NewErrorResponse("", err), nil
86 }
87
88 req.Domain = strings.TrimSpace(req.Domain)
89 if req.Domain == "" {
90 return NewErrorResponse(req.RequestID, fmt.Errorf("domain is required")), nil
91 }
92
93 log.Debug().
94 Str("request_id", req.RequestID).
95 Str("domain", req.Domain).
96 Int("limit", req.Limit).
97 Int("offset", req.Offset).
98 Msg("handling history_timeline_by_domain")
99 if req.Limit <= 0 {
100 return NewErrorResponse(
101 req.RequestID,
102 fmt.Errorf("history_timeline_by_domain requires a positive limit; use history_timeline_window for lazy history loading"),
103 ), nil
104 }
105
106 entries, err := h.historyUC.GetRecentByDomain(ctx, req.Domain, req.Limit, req.Offset)
107 if err != nil {
108 return NewErrorResponse(req.RequestID, err), nil
109 }
110
111 return NewSuccessResponse(req.RequestID, entries), nil
112 })
113}
114
115// HandleTimelineWindow handles history_timeline_window messages.
116func (h *HistoryHandlers) HandleTimelineWindow() port.WebUIMessageHandler {
117 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
118 log := logging.FromContext(ctx)
119
120 var req timelineWindowRequest
121 if err := json.Unmarshal(payload, &req); err != nil {
122 return NewErrorResponse("", err), nil
123 }
124
125 domain := strings.TrimSpace(req.Domain)
126
127 var before time.Time
128 hasBefore := strings.TrimSpace(req.Before) != ""
129 if req.BeforeID < 0 {
130 return NewErrorResponse(req.RequestID, fmt.Errorf("history window cursor id must be non-negative")), nil
131 }
132 hasBeforeID := req.BeforeID > 0
133 if hasBefore != hasBeforeID {
134 return NewErrorResponse(req.RequestID, fmt.Errorf("history window cursor requires before and beforeId")), nil
135 }
136 if hasBefore {
137 parsed, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(req.Before))
138 if err != nil {
139 return NewErrorResponse(req.RequestID, fmt.Errorf("invalid history window cursor")), nil
140 }
141 before = parsed
142 }
143
144 log.Debug().
145 Str("request_id", req.RequestID).
146 Str("domain", domain).
147 Time("before", before).
148 Msg("handling history_timeline_window")
149
150 window, err := h.historyUC.GetRecentWindow(ctx, before, req.BeforeID, domain)
151 if err != nil {
152 return NewErrorResponse(req.RequestID, err), nil
153 }
154
155 return NewSuccessResponse(req.RequestID, window), nil
156 })
157}
158
159// searchRequest is the payload for history_search_fts messages.
160type searchRequest struct {
161 RequestID string `json:"requestId"`
162 Query string `json:"query"`
163 Limit int `json:"limit"`
164}
165
166// HandleSearchFTS handles history_search_fts messages.
167func (h *HistoryHandlers) HandleSearchFTS() port.WebUIMessageHandler {
168 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
169 log := logging.FromContext(ctx)
170
171 var req searchRequest
172 if err := json.Unmarshal(payload, &req); err != nil {
173 return NewErrorResponse("", err), nil
174 }
175
176 log.Debug().
177 Str("request_id", req.RequestID).
178 Str("query", req.Query).
179 Msg("handling history_search_fts")
180
181 output, err := h.historyUC.Search(ctx, dto.HistorySearchInput{
182 Query: req.Query,
183 Limit: req.Limit,
184 })
185 if err != nil {
186 return NewErrorResponse(req.RequestID, err), nil
187 }
188
189 // Convert matches to entries for frontend compatibility
190 entries := make([]*entity.HistoryEntry, 0, len(output.Matches))
191 for _, m := range output.Matches {
192 entries = append(entries, m.Entry)
193 }
194
195 return NewSuccessResponse(req.RequestID, entries), nil
196 })
197}
198
199// deleteEntryRequest is the payload for history_delete_entry messages.
200type deleteEntryRequest struct {
201 RequestID string `json:"requestId"`
202 ID int64 `json:"id"`
203}
204
205// HandleDeleteEntry handles history_delete_entry messages.
206func (h *HistoryHandlers) HandleDeleteEntry() port.WebUIMessageHandler {
207 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
208 log := logging.FromContext(ctx)
209
210 var req deleteEntryRequest
211 if err := json.Unmarshal(payload, &req); err != nil {
212 return NewErrorResponse("", err), nil
213 }
214
215 log.Debug().
216 Str("request_id", req.RequestID).
217 Int64("id", req.ID).
218 Msg("handling history_delete_entry")
219
220 if err := h.historyUC.Delete(ctx, req.ID); err != nil {
221 return NewErrorResponse(req.RequestID, err), nil
222 }
223
224 return NewSuccessResponse(req.RequestID, nil), nil
225 })
226}
227
228// deleteRangeRequest is the payload for history_delete_range messages.
229type deleteRangeRequest struct {
230 RequestID string `json:"requestId"`
231 Range string `json:"range"` // "hour", "day", "week", "month", "all"
232}
233
234// HandleDeleteRange handles history_delete_range messages.
235func (h *HistoryHandlers) HandleDeleteRange() port.WebUIMessageHandler {
236 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
237 log := logging.FromContext(ctx)
238
239 var req deleteRangeRequest
240 if err := json.Unmarshal(payload, &req); err != nil {
241 return NewErrorResponse("", err), nil
242 }
243
244 req.Range = strings.TrimSpace(req.Range)
245 log.Debug().
246 Str("request_id", req.RequestID).
247 Str("range", req.Range).
248 Msg("handling history_delete_range")
249
250 if err := h.historyUC.ClearRange(ctx, req.Range); err != nil {
251 return NewErrorResponse(req.RequestID, err), nil
252 }
253
254 return NewSuccessResponse(req.RequestID, nil), nil
255 })
256}
257
258// HandleClearAll handles history_clear_all messages.
259func (h *HistoryHandlers) HandleClearAll() port.WebUIMessageHandler {
260 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
261 log := logging.FromContext(ctx)
262
263 requestID := ParseRequestID(payload)
264
265 log.Debug().
266 Str("request_id", requestID).
267 Msg("handling history_clear_all")
268
269 if err := h.historyUC.ClearAll(ctx); err != nil {
270 return NewErrorResponse(requestID, err), nil
271 }
272
273 return NewSuccessResponse(requestID, nil), nil
274 })
275}
276
277// HandleStats handles history_stats messages.
278func (h *HistoryHandlers) HandleStats() port.WebUIMessageHandler {
279 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
280 log := logging.FromContext(ctx)
281
282 var req struct {
283 RequestID string `json:"requestId"`
284 }
285 if err := json.Unmarshal(payload, &req); err != nil {
286 return NewErrorResponse("", err), nil
287 }
288
289 log.Debug().Str("request_id", req.RequestID).Msg("handling history_stats")
290
291 stats, err := h.historyUC.GetStats(ctx)
292 if err != nil {
293 return NewErrorResponse(req.RequestID, err), nil
294 }
295
296 return NewSuccessResponse(req.RequestID, stats), nil
297 })
298}
299
300// HandleAnalytics handles history_analytics messages.
301func (h *HistoryHandlers) HandleAnalytics() port.WebUIMessageHandler {
302 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
303 log := logging.FromContext(ctx)
304
305 requestID := ParseRequestID(payload)
306
307 log.Debug().
308 Str("request_id", requestID).
309 Msg("handling history_analytics")
310
311 analytics, err := h.historyUC.GetAnalytics(ctx)
312 if err != nil {
313 return NewErrorResponse(requestID, err), nil
314 }
315
316 return NewSuccessResponse(requestID, analytics), nil
317 })
318}
319
320// domainStatsRequest is the payload for history_domain_stats messages.
321type domainStatsRequest struct {
322 RequestID string `json:"requestId"`
323 Limit int `json:"limit"`
324}
325
326// HandleDomainStats handles history_domain_stats messages.
327func (h *HistoryHandlers) HandleDomainStats() port.WebUIMessageHandler {
328 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
329 log := logging.FromContext(ctx)
330
331 var req domainStatsRequest
332 if err := json.Unmarshal(payload, &req); err != nil {
333 return NewErrorResponse("", err), nil
334 }
335
336 log.Debug().
337 Str("request_id", req.RequestID).
338 Int("limit", req.Limit).
339 Msg("handling history_domain_stats")
340
341 stats, err := h.historyUC.GetDomainStats(ctx, req.Limit)
342 if err != nil {
343 return NewErrorResponse(req.RequestID, err), nil
344 }
345
346 return NewSuccessResponse(req.RequestID, stats), nil
347 })
348}
349
350// deleteDomainRequest is the payload for history_delete_domain messages.
351type deleteDomainRequest struct {
352 RequestID string `json:"requestId"`
353 Domain string `json:"domain"`
354}
355
356// HandleDeleteDomain handles history_delete_domain messages.
357func (h *HistoryHandlers) HandleDeleteDomain() port.WebUIMessageHandler {
358 return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
359 log := logging.FromContext(ctx)
360
361 var req deleteDomainRequest
362 if err := json.Unmarshal(payload, &req); err != nil {
363 return NewErrorResponse("", err), nil
364 }
365
366 log.Debug().
367 Str("request_id", req.RequestID).
368 Str("domain", req.Domain).
369 Msg("handling history_delete_domain")
370
371 if err := h.historyUC.DeleteByDomain(ctx, req.Domain); err != nil {
372 return NewErrorResponse(req.RequestID, err), nil
373 }
374
375 return NewSuccessResponse(req.RequestID, nil), nil
376 })
377}