diff --git a/cmd/server/main.go b/cmd/server/main.go index f36a87f..aed718e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -61,11 +61,15 @@ func main() { wsHandler := handler.NewWSHandler(hub, authService) adminHandler := handler.NewAdminHandler(userRepo, authService) - // Start background feed fetcher + // Start background workers fetcher := worker.NewFeedFetcher(fetchService, 15*time.Minute, 5) fetcher.Start() defer fetcher.Stop() + cleaner := worker.NewCleaner(articleRepo, 24*time.Hour) + cleaner.Start() + defer cleaner.Stop() + // Initialize router r := chi.NewRouter() diff --git a/internal/repository/article.go b/internal/repository/article.go index 42da6c8..6478e41 100644 --- a/internal/repository/article.go +++ b/internal/repository/article.go @@ -521,6 +521,23 @@ func (r *ArticleRepository) UpdateAISummary(id uuid.UUID, summary string) error return nil } +// DeleteOldArticles removes articles older than the specified duration, except for favorites. +func (r *ArticleRepository) DeleteOldArticles(ctx context.Context, olderThan time.Duration) (int64, error) { + threshold := time.Now().Add(-olderThan) + + query := ` + DELETE FROM articles + WHERE created_at < $1 AND is_favorite = false + ` + + result, err := r.pool.Exec(ctx, query, threshold) + if err != nil { + return 0, fmt.Errorf("deleting old articles: %w", err) + } + + return result.RowsAffected(), nil +} + // nullString returns nil if string is empty. func nullString(s string) *string { if s == "" { diff --git a/internal/worker/cleaner.go b/internal/worker/cleaner.go new file mode 100644 index 0000000..a867333 --- /dev/null +++ b/internal/worker/cleaner.go @@ -0,0 +1,76 @@ +package worker + +import ( + "context" + "log" + "sync" + "time" + + "github.com/michael/flowreader/internal/repository" +) + +// Cleaner handles periodic database maintenance. +type Cleaner struct { + repo *repository.ArticleRepository + interval time.Duration + stopCh chan struct{} + wg sync.WaitGroup +} + +// NewCleaner creates a new database cleaner worker. +func NewCleaner(repo *repository.ArticleRepository, interval time.Duration) *Cleaner { + return &Cleaner{ + repo: repo, + interval: interval, + stopCh: make(chan struct{}), + } +} + +// Start begins the background maintenance loop. +func (c *Cleaner) Start() { + c.wg.Add(1) + go c.run() + log.Printf("Maintenance worker started (interval: %s)", c.interval) +} + +// Stop gracefully stops the cleaner. +func (c *Cleaner) Stop() { + close(c.stopCh) + c.wg.Wait() + log.Println("Maintenance worker stopped") +} + +func (c *Cleaner) run() { + defer c.wg.Done() + + // Initial cleanup on startup + c.cleanup() + + ticker := time.NewTicker(c.interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + c.cleanup() + case <-c.stopCh: + return + } + } +} + +func (c *Cleaner) cleanup() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + // Delete articles older than 30 days + count, err := c.repo.DeleteOldArticles(ctx, 30*24*time.Hour) + if err != nil { + log.Printf("Maintenance cleanup error: %v", err) + return + } + + if count > 0 { + log.Printf("Maintenance: cleaned up %d old articles", count) + } +} diff --git a/web/src/components/ArticleCard.tsx b/web/src/components/ArticleCard.tsx index bda4766..cf96d18 100644 --- a/web/src/components/ArticleCard.tsx +++ b/web/src/components/ArticleCard.tsx @@ -150,15 +150,14 @@ export function ArticleCard({ article, onClick, onToggleRead, onToggleFavorite } {article.ai_summary ? ( -
- ✨ -

- {article.ai_summary} -

+
+ + ✨ Smart Digest prêt +
) : article.summary && ( -

- {article.summary.replace(/<[^>]*>?/gm, '').substring(0, 160)}... +

+ {article.summary.replace(/<[^>]*>?/gm, '').substring(0, 120)}...

)} diff --git a/web/src/components/ReaderView.tsx b/web/src/components/ReaderView.tsx index f36ae16..5970cc9 100644 --- a/web/src/components/ReaderView.tsx +++ b/web/src/components/ReaderView.tsx @@ -1,4 +1,4 @@ -import type { Article } from '../api/articles'; +import { type Article, articlesApi } from '../api/articles'; interface ReaderViewProps { article: Article; @@ -59,9 +59,39 @@ export function ReaderView({ article, onClose, onToggleFavorite }: ReaderViewPro {article.published_at ? new Date(article.published_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }) : 'Aujourd\'hui'}
-

+

{article.title}

+ + {/* Smart Digest Section */} + {article.ai_summary ? ( +
+

+ ✨ Smart Digest +

+

+ {article.ai_summary} +

+
+ ) : ( +
+ +
+ )}