mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-23 07:08:06 +02:00
feat: auto-cleanup articles > 30d and relocate AI summary to reader view
This commit is contained in:
1 parent
b3cbaf741d
commit
c4cc7eaf87
5 files changed
+136
-10
No files matched your search
+5
-1
@@ -61,11 +61,15 @@ func main() {
|
|||||||
wsHandler := handler.NewWSHandler(hub, authService)
|
wsHandler := handler.NewWSHandler(hub, authService)
|
||||||
adminHandler := handler.NewAdminHandler(userRepo, authService)
|
adminHandler := handler.NewAdminHandler(userRepo, authService)
|
||||||
|
|
||||||
// Start background feed fetcher
|
// Start background workers
|
||||||
fetcher := worker.NewFeedFetcher(fetchService, 15*time.Minute, 5)
|
fetcher := worker.NewFeedFetcher(fetchService, 15*time.Minute, 5)
|
||||||
fetcher.Start()
|
fetcher.Start()
|
||||||
defer fetcher.Stop()
|
defer fetcher.Stop()
|
||||||
|
|
||||||
|
cleaner := worker.NewCleaner(articleRepo, 24*time.Hour)
|
||||||
|
cleaner.Start()
|
||||||
|
defer cleaner.Stop()
|
||||||
|
|
||||||
// Initialize router
|
// Initialize router
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -521,6 +521,23 @@ func (r *ArticleRepository) UpdateAISummary(id uuid.UUID, summary string) error
|
|||||||
return nil
|
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.
|
// nullString returns nil if string is empty.
|
||||||
func nullString(s string) *string {
|
func nullString(s string) *string {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -150,15 +150,14 @@ export function ArticleCard({ article, onClick, onToggleRead, onToggleFavorite }
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{article.ai_summary ? (
|
{article.ai_summary ? (
|
||||||
<div className="bg-nature/5 border-l-4 border-nature p-4 rounded-r-lg animate-fade-in relative my-2">
|
<div className="flex items-center space-x-2 text-nature/80">
|
||||||
<span className="absolute -top-3 left-2 text-xl filter drop-shadow-sm">✨</span>
|
<span className="text-sm px-2 py-0.5 bg-nature/10 rounded-full font-medium flex items-center">
|
||||||
<p className="text-paper-white text-sm leading-relaxed font-reading italic">
|
<span className="mr-1">✨</span> Smart Digest prêt
|
||||||
{article.ai_summary}
|
</span>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : article.summary && (
|
) : article.summary && (
|
||||||
<p className="text-paper-muted/90 text-sm leading-relaxed line-clamp-3 font-reading group-hover:text-paper-white transition-colors">
|
<p className="text-paper-muted/90 text-sm leading-relaxed line-clamp-2 font-reading group-hover:text-paper-white transition-colors">
|
||||||
{article.summary.replace(/<[^>]*>?/gm, '').substring(0, 160)}...
|
{article.summary.replace(/<[^>]*>?/gm, '').substring(0, 120)}...
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Article } from '../api/articles';
|
import { type Article, articlesApi } from '../api/articles';
|
||||||
|
|
||||||
interface ReaderViewProps {
|
interface ReaderViewProps {
|
||||||
article: Article;
|
article: Article;
|
||||||
@@ -59,9 +59,39 @@ export function ReaderView({ article, onClose, onToggleFavorite }: ReaderViewPro
|
|||||||
<span>{article.published_at ? new Date(article.published_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }) : 'Aujourd\'hui'}</span>
|
<span>{article.published_at ? new Date(article.published_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }) : 'Aujourd\'hui'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-6xl font-serif text-paper-white leading-[1.15] italic-nature tracking-tight">
|
<h1 className="text-4xl md:text-6xl font-serif text-paper-white leading-[1.15] italic-nature tracking-tight mb-8">
|
||||||
{article.title}
|
{article.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
{/* Smart Digest Section */}
|
||||||
|
{article.ai_summary ? (
|
||||||
|
<div className="bg-nature/5 border-l-4 border-nature p-6 rounded-r-2xl mb-12 animate-fade-in shadow-sm">
|
||||||
|
<h3 className="text-nature font-black text-[10px] uppercase tracking-[0.2em] mb-4 flex items-center">
|
||||||
|
<span className="mr-2 text-base">✨</span> Smart Digest
|
||||||
|
</h3>
|
||||||
|
<p className="text-paper-white text-lg leading-relaxed font-reading italic">
|
||||||
|
{article.ai_summary}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mb-12">
|
||||||
|
<button
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
try {
|
||||||
|
await articlesApi.summarize(article.id);
|
||||||
|
// The WS hub will trigger an update, but we could also show a loading state here
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to summarize:", err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="group flex items-center space-x-3 bg-nature/5 hover:bg-nature text-nature hover:text-white border border-nature/20 px-6 py-3 rounded-full transition-all duration-500"
|
||||||
|
>
|
||||||
|
<span className="text-xl group-hover:rotate-12 transition-transform">✨</span>
|
||||||
|
<span className="text-[10px] uppercase tracking-[0.2em] font-black">Générer le Smart Digest</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in new issue
Block a user