diff --git a/cmd/remembos/main.go b/cmd/remembos/main.go index 4df63f0..ffb2244 100644 --- a/cmd/remembos/main.go +++ b/cmd/remembos/main.go @@ -66,7 +66,7 @@ func main() { selector := search.NewSelector(client, store, &cfg.Search, loc, logger) // Memory service - memorySvc := memory.NewService(selector, store, loc, logger) + memorySvc := memory.NewService(selector, store, client, loc, logger) // Web handler handler := web.NewHandler(memorySvc, cfg.Memos.URL, cfg.Memos.PublicURL, cfg.General.AllowLoadMore, logger) diff --git a/internal/memory/service.go b/internal/memory/service.go index c8a6112..8241efc 100644 --- a/internal/memory/service.go +++ b/internal/memory/service.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "git.vakhrushev.me/av/remembos/internal/memos" "git.vakhrushev.me/av/remembos/internal/search" "git.vakhrushev.me/av/remembos/internal/storage" ) @@ -14,6 +15,7 @@ import ( type Service struct { selector *search.Selector store *storage.Storage + client *memos.Client loc *time.Location logger *slog.Logger @@ -22,16 +24,18 @@ type Service struct { cached *search.Memory } -func NewService(selector *search.Selector, store *storage.Storage, loc *time.Location, logger *slog.Logger) *Service { +func NewService(selector *search.Selector, store *storage.Storage, client *memos.Client, loc *time.Location, logger *slog.Logger) *Service { return &Service{ selector: selector, store: store, + client: client, loc: loc, logger: logger, } } // GetTodayMemory returns the memory for today, caching the result. +// On cache miss (e.g. after restart), it checks the DB for a memo already shown today. func (s *Service) GetTodayMemory(ctx context.Context) (*search.Memory, error) { today := time.Now().In(s.loc) dayKey := today.Format("2006-01-02") @@ -44,6 +48,38 @@ func (s *Service) GetTodayMemory(ctx context.Context) (*search.Memory, error) { } s.mu.Unlock() + // Try to restore from DB — check if a memo was already shown today + dayStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, s.loc) + dayEnd := dayStart.AddDate(0, 0, 1) + + memoName, tier, err := s.store.GetLastShownToday(ctx, dayStart.Unix(), dayEnd.Unix()) + if err != nil { + s.logger.Error("failed to check today's show history", "error", err) + // Fall through to select a new one + } + + if memoName != "" { + s.logger.Info("restoring today's memory from history", "memo", memoName) + + memo, err := s.client.GetMemo(ctx, memoName) + if err != nil { + s.logger.Error("failed to fetch memo from history, selecting new", "memo", memoName, "error", err) + } else { + mem := &search.Memory{ + Memo: memo, + Tier: tier, + Date: memo.DisplayTime, + } + + s.mu.Lock() + s.cacheDay = dayKey + s.cached = mem + s.mu.Unlock() + + return mem, nil + } + } + s.logger.Info("selecting new memory", "date", dayKey) mem, err := s.selector.Select(ctx, today) @@ -58,7 +94,6 @@ func (s *Service) GetTodayMemory(ctx context.Context) (*search.Memory, error) { if err := s.store.RecordShow(ctx, mem.Memo.Name, mem.Tier); err != nil { s.logger.Error("failed to record show", "error", err) - // Non-fatal: still return the memory } s.mu.Lock() diff --git a/internal/memos/client.go b/internal/memos/client.go index f9c695d..633779c 100644 --- a/internal/memos/client.go +++ b/internal/memos/client.go @@ -67,6 +67,34 @@ func (c *Client) ListMemos(ctx context.Context, filter string, pageSize int, pag return &result, nil } +// GetMemo fetches a single memo by its resource name (e.g. "memos/123"). +func (c *Client) GetMemo(ctx context.Context, name string) (*Memo, error) { + reqURL := fmt.Sprintf("%s/api/v1/%s", c.baseURL, name) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, http.NoBody) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+c.token) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("do request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("memos API returned %d: %s", resp.StatusCode, body) + } + + var memo Memo + if err := json.NewDecoder(resp.Body).Decode(&memo); err != nil { + return nil, fmt.Errorf("decode memo: %w", err) + } + return &memo, nil +} + // DownloadAttachment downloads the attachment data as bytes. func (c *Client) DownloadAttachment(ctx context.Context, att Attachment) ([]byte, error) { var reqURL string diff --git a/internal/storage/history.go b/internal/storage/history.go index 8d0cac0..5793962 100644 --- a/internal/storage/history.go +++ b/internal/storage/history.go @@ -2,6 +2,8 @@ package storage import ( "context" + "database/sql" + "errors" "fmt" "strings" "time" @@ -65,6 +67,21 @@ func (s *Storage) GetShowCounts(ctx context.Context, memoNames []string) (map[st return result, rows.Err() } +// GetLastShownToday returns the memo_name and tier of the most recently shown memo +// within the given time range [from, to). Returns empty string if nothing was shown. +func (s *Storage) GetLastShownToday(ctx context.Context, from, to int64) (memoName string, tier int, err error) { + err = s.db.QueryRowContext(ctx, + `SELECT memo_name, tier FROM show_history WHERE shown_at >= ? AND shown_at < ? ORDER BY shown_at DESC LIMIT 1`, + from, to).Scan(&memoName, &tier) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return "", 0, nil + } + return "", 0, fmt.Errorf("get last shown today: %w", err) + } + return memoName, tier, nil +} + // RecordShow records that a memo was shown. func (s *Storage) RecordShow(ctx context.Context, memoName string, tier int) error { _, err := s.db.ExecContext(ctx,