From 007aec209439c9a9cb75f820c3c949dbdbdd4edd Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 4 Feb 2026 16:40:04 +0100 Subject: [PATCH] fix: add immediate feed fetch, refresh api and frontend update button --- cmd/server/main.go | 3 ++- internal/handler/feed.go | 44 +++++++++++++++++++++++++++++---- web/src/api/feeds.ts | 8 ++++++ web/src/pages/DashboardPage.tsx | 29 +++++++++++++++++++--- 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 89cca88..475e982 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -55,7 +55,7 @@ func main() { // Initialize handlers authHandler := handler.NewAuthHandler(authService) - feedHandler := handler.NewFeedHandler(feedService, authService) + feedHandler := handler.NewFeedHandler(feedService, fetchService, authService) articleHandler := handler.NewArticleHandler(articleRepo, feedService, authService, hub) wsHandler := handler.NewWSHandler(hub, authService) adminHandler := handler.NewAdminHandler(userRepo, authService) @@ -109,6 +109,7 @@ func main() { r.Route("/feeds", func(r chi.Router) { r.Get("/", feedHandler.List) r.Post("/", feedHandler.Add) + r.Post("/refresh", feedHandler.Refresh) r.Post("/import/opml", feedHandler.ImportOPML) r.Get("/export/opml", feedHandler.ExportOPML) r.Get("/{id}", feedHandler.Get) diff --git a/internal/handler/feed.go b/internal/handler/feed.go index 0afdfc4..8e27f5d 100644 --- a/internal/handler/feed.go +++ b/internal/handler/feed.go @@ -1,9 +1,11 @@ package handler import ( + "context" "encoding/json" "errors" "net/http" + "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" @@ -13,15 +15,17 @@ import ( // FeedHandler handles feed-related HTTP requests. type FeedHandler struct { - feedService *service.FeedService - authService *service.AuthService + feedService *service.FeedService + fetchService *service.FetchService + authService *service.AuthService } // NewFeedHandler creates a new feed handler. -func NewFeedHandler(feedService *service.FeedService, authService *service.AuthService) *FeedHandler { +func NewFeedHandler(feedService *service.FeedService, fetchService *service.FetchService, authService *service.AuthService) *FeedHandler { return &FeedHandler{ - feedService: feedService, - authService: authService, + feedService: feedService, + fetchService: fetchService, + authService: authService, } } @@ -85,9 +89,39 @@ func (h *FeedHandler) Add(w http.ResponseWriter, r *http.Request) { return } + // Trigger immediate fetch in background + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + _ = h.fetchService.FetchFeed(ctx, resp.ID) + }() + respondJSON(w, http.StatusCreated, resp) } +// Refresh handles POST /api/v1/feeds/refresh +func (h *FeedHandler) Refresh(w http.ResponseWriter, r *http.Request) { + userID, err := h.getUserFromRequest(r) + if err != nil { + respondError(w, http.StatusUnauthorized, err.Error()) + return + } + + // For now, we refresh all feeds for the user synchronously or in background + // Let's do background and return 202 Accepted + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + feeds, _ := h.feedService.GetUserFeeds(userID) + for _, f := range feeds { + _ = h.fetchService.FetchFeed(ctx, f.ID) + } + }() + + respondJSON(w, http.StatusAccepted, map[string]string{"message": "Refresh started"}) +} + // Get handles GET /api/v1/feeds/{id} func (h *FeedHandler) Get(w http.ResponseWriter, r *http.Request) { userID, err := h.getUserFromRequest(r) diff --git a/web/src/api/feeds.ts b/web/src/api/feeds.ts index 0f80804..1eabd11 100644 --- a/web/src/api/feeds.ts +++ b/web/src/api/feeds.ts @@ -52,6 +52,14 @@ export const feedsApi = { await handleResponse(response); }, + async refresh(): Promise<{ message: string }> { + const response = await fetch(`${API_BASE}/feeds/refresh`, { + method: 'POST', + credentials: 'include', + }); + return handleResponse(response); + }, + async importOPML(file: File): Promise<{ imported: number; skipped: number; errors?: string[] }> { const formData = new FormData(); formData.append('file', file); diff --git a/web/src/pages/DashboardPage.tsx b/web/src/pages/DashboardPage.tsx index 67a03e4..231e0fb 100644 --- a/web/src/pages/DashboardPage.tsx +++ b/web/src/pages/DashboardPage.tsx @@ -1,8 +1,10 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { articlesApi } from '../api/articles'; +import { feedsApi } from '../api/feeds'; import { ArticleCard } from '../components/ArticleCard'; import { ReaderView } from '../components/ReaderView'; +import { useWebsocket } from '../hooks/useWebsocket'; import type { Article } from '../api/articles'; interface DashboardPageProps { @@ -13,11 +15,23 @@ export function DashboardPage({ selectedFeedId }: DashboardPageProps) { const queryClient = useQueryClient(); const [selectedArticle, setSelectedArticle] = useState
(null); + // Real-time sync + useWebsocket(); + const { data: articles, isLoading } = useQuery({ queryKey: ['articles', selectedFeedId], queryFn: () => articlesApi.list({ feed_id: selectedFeedId || undefined }), }); + const refreshMutation = useMutation({ + mutationFn: () => feedsApi.refresh(), + onSuccess: () => { + // Optimistic feedback or just wait for WS? + // In case WS fails, we invalid articles + queryClient.invalidateQueries({ queryKey: ['articles'] }); + }, + }); + const toggleReadMutation = useMutation({ mutationFn: ({ id, is_read }: { id: string; is_read: boolean }) => is_read ? articlesApi.markRead(id) : articlesApi.markUnread(id), @@ -50,9 +64,18 @@ export function DashboardPage({ selectedFeedId }: DashboardPageProps) { {selectedFeedId ? 'Archives du Flux' : 'La Une'} -
-

Mercredi 4 Février 2026

-

Volume IX • No. 42

+
+
+

Mercredi 4 Février 2026

+

Volume IX • No. 42

+
+