nandi/gleanpublic Fork 0
9f09d5b
Commits
Clone
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Implement async feed refresh with polling and category filteringUnverified

Julien Robert committed 2026-04-23T22:21:01+02:00 Browse files
9f09d5b parent: 6be8ad9
modified internal/server/feeds_handler.go +23 -14
@@ -338,11 +338,12 @@ func (s *Server) handleOPMLDownload(w http.ResponseWriter, r *http.Request) {
338338
339339 func (s *Server) handleFeedList(w http.ResponseWriter, r *http.Request) {
340340 user := currentUser(r)
341- subs, err := s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, "", 100, 0)
341+ category := r.URL.Query().Get("category")
342+ subs, err := s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, category, 100, 0)
342343 if err != nil {
343344 s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
344345 }
345- s.render(w, r, "feeds.html", map[string]any{
346+ s.render(w, r, "feed-list.html", map[string]any{
346347 "User": user,
347348 "Subscriptions": subs,
348349 })
@@ -350,11 +351,28 @@ func (s *Server) handleFeedList(w http.ResponseWriter, r *http.Request) {
350351
351352 func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
352353 user := currentUser(r)
354+ ctx := r.Context()
355+
356+ go s.refreshUserFeeds(context.WithoutCancel(ctx), user.DID)
353357
354- subs, err := s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, "", 100, 0)
358+ category := r.URL.Query().Get("category")
359+ subs, err := s.dbs.Articles.ListSubscriptions(ctx, user.DID, category, 100, 0)
355360 if err != nil {
356361 s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
357362 }
363+ s.render(w, r, "feed-list.html", map[string]any{
364+ "User": user,
365+ "Subscriptions": subs,
366+ })
367+}
368+
369+func (s *Server) refreshUserFeeds(ctx context.Context, userDID string) {
370+ subs, err := s.dbs.Articles.ListSubscriptions(ctx, userDID, "", 100, 0)
371+ if err != nil {
372+ s.logger.Warn("failed to list subscriptions for refresh", "error", err, "did", userDID)
373+ return
374+ }
375+
358376 seen := make(map[string]bool)
359377 for _, sub := range subs {
360378 if seen[sub.FeedURL] {
@@ -362,7 +380,7 @@ func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
362380 }
363381 seen[sub.FeedURL] = true
364382
365- f, err := s.dbs.Articles.GetFeed(r.Context(), sub.FeedURL)
383+ f, err := s.dbs.Articles.GetFeed(ctx, sub.FeedURL)
366384 if err != nil {
367385 s.logger.Warn("failed to get feed", "error", err, "feed", sub.FeedURL)
368386 continue
@@ -376,17 +394,8 @@ func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
376394 ETag: f.Etag.String,
377395 LastModified: f.LastModified.String,
378396 }
379- s.scheduler.FetchFeed(r.Context(), ff)
380- }
381-
382- subs, err = s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, "", 100, 0)
383- if err != nil {
384- s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
397+ s.scheduler.FetchFeed(ctx, ff)
385398 }
386- s.render(w, r, "feed-list.html", map[string]any{
387- "User": user,
388- "Subscriptions": subs,
389- })
390399 }
391400
392401 func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
@@ -338,11 +338,12 @@ func (s *Server) handleOPMLDownload(w http.ResponseWriter, r *http.Request) {
338 338
339 func (s *Server) handleFeedList(w http.ResponseWriter, r *http.Request) {339 func (s *Server) handleFeedList(w http.ResponseWriter, r *http.Request) {
340 user := currentUser(r)340 user := currentUser(r)
341- subs, err := s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, "", 100, 0)341+ category := r.URL.Query().Get("category")
342+ subs, err := s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, category, 100, 0)
342 if err != nil {343 if err != nil {
343 s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)344 s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
344 }345 }
345- s.render(w, r, "feeds.html", map[string]any{346+ s.render(w, r, "feed-list.html", map[string]any{
346 "User": user,347 "User": user,
347 "Subscriptions": subs,348 "Subscriptions": subs,
348 })349 })
@@ -350,11 +351,28 @@ func (s *Server) handleFeedList(w http.ResponseWriter, r *http.Request) {
350 351
351 func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {352 func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
352 user := currentUser(r)353 user := currentUser(r)
354+ ctx := r.Context()
355+
356+ go s.refreshUserFeeds(context.WithoutCancel(ctx), user.DID)
353 357
354- subs, err := s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, "", 100, 0)358+ category := r.URL.Query().Get("category")
359+ subs, err := s.dbs.Articles.ListSubscriptions(ctx, user.DID, category, 100, 0)
355 if err != nil {360 if err != nil {
356 s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)361 s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
357 }362 }
363+ s.render(w, r, "feed-list.html", map[string]any{
364+ "User": user,
365+ "Subscriptions": subs,
366+ })
367+}
368+
369+func (s *Server) refreshUserFeeds(ctx context.Context, userDID string) {
370+ subs, err := s.dbs.Articles.ListSubscriptions(ctx, userDID, "", 100, 0)
371+ if err != nil {
372+ s.logger.Warn("failed to list subscriptions for refresh", "error", err, "did", userDID)
373+ return
374+ }
375+
358 seen := make(map[string]bool)376 seen := make(map[string]bool)
359 for _, sub := range subs {377 for _, sub := range subs {
360 if seen[sub.FeedURL] {378 if seen[sub.FeedURL] {
@@ -362,7 +380,7 @@ func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
362 }380 }
363 seen[sub.FeedURL] = true381 seen[sub.FeedURL] = true
364 382
365- f, err := s.dbs.Articles.GetFeed(r.Context(), sub.FeedURL)383+ f, err := s.dbs.Articles.GetFeed(ctx, sub.FeedURL)
366 if err != nil {384 if err != nil {
367 s.logger.Warn("failed to get feed", "error", err, "feed", sub.FeedURL)385 s.logger.Warn("failed to get feed", "error", err, "feed", sub.FeedURL)
368 continue386 continue
@@ -376,17 +394,8 @@ func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
376 ETag: f.Etag.String,394 ETag: f.Etag.String,
377 LastModified: f.LastModified.String,395 LastModified: f.LastModified.String,
378 }396 }
379- s.scheduler.FetchFeed(r.Context(), ff)397+ s.scheduler.FetchFeed(ctx, ff)
380- }
381-
382- subs, err = s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, "", 100, 0)
383- if err != nil {
384- s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
385 }398 }
386- s.render(w, r, "feed-list.html", map[string]any{
387- "User": user,
388- "Subscriptions": subs,
389- })
390 }399 }
391 400
392 func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {401 func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
modified internal/tmpl/feeds.html +10 -4
@@ -2,14 +2,20 @@
22 <div class="flex items-center justify-between gap-3 mb-2 flex-wrap">
33 <h1 class="text-2xl font-bold text-spot-text">Feeds <span class="text-base font-normal text-spot-secondary">({{.SubscriptionCount}})</span></h1>
44 <div class="flex items-center gap-3">
5- <button hx-post="/feeds/refresh" hx-target="#feed-list" hx-swap="innerHTML"
6- hx-indicator="#refresh-indicator"
7- class="border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition">
5+ <button id="refresh-btn"
6+ hx-post="/feeds/refresh" hx-target="#feed-list" hx-swap="innerHTML"
7+ hx-on::before-request="gleanRefreshStart()"
8+ hx-on::after-request="gleanRefreshPoll()"
9+ class="border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition disabled:opacity-50">
810 Refresh feeds
911 </button>
10- <span id="refresh-indicator" class="htmx-indicator text-sm text-spot-secondary">Fetching...</span>
12+ <span id="refresh-indicator" class="text-sm text-spot-secondary" style="display:none">Refreshing feeds...</span>
1113 </div>
1214 </div>
15+<script>
16+function gleanRefreshStart(){document.getElementById('refresh-indicator').style.display='inline';document.getElementById('refresh-btn').disabled=true}
17+function gleanRefreshPoll(){setTimeout(function(){htmx.ajax('GET','/feeds/list',{target:'#feed-list',swap:'innerHTML'});document.getElementById('refresh-indicator').style.display='none';document.getElementById('refresh-btn').disabled=false},5000)}
18+</script>
1319 <p class="text-sm text-spot-secondary mb-6">Manage your RSS and Atom subscriptions.</p>
1420
1521 {{template "dead-feeds.html" (dict "DeadFeeds" .DeadFeeds "CSRFToken" .CSRFToken)}}
@@ -2,14 +2,20 @@
2 <div class="flex items-center justify-between gap-3 mb-2 flex-wrap">2 <div class="flex items-center justify-between gap-3 mb-2 flex-wrap">
3 <h1 class="text-2xl font-bold text-spot-text">Feeds <span class="text-base font-normal text-spot-secondary">({{.SubscriptionCount}})</span></h1>3 <h1 class="text-2xl font-bold text-spot-text">Feeds <span class="text-base font-normal text-spot-secondary">({{.SubscriptionCount}})</span></h1>
4 <div class="flex items-center gap-3">4 <div class="flex items-center gap-3">
5- <button hx-post="/feeds/refresh" hx-target="#feed-list" hx-swap="innerHTML"5+ <button id="refresh-btn"
6- hx-indicator="#refresh-indicator"6+ hx-post="/feeds/refresh" hx-target="#feed-list" hx-swap="innerHTML"
7- class="border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition">7+ hx-on::before-request="gleanRefreshStart()"
8+ hx-on::after-request="gleanRefreshPoll()"
9+ class="border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition disabled:opacity-50">
8 Refresh feeds10 Refresh feeds
9 </button>11 </button>
10- <span id="refresh-indicator" class="htmx-indicator text-sm text-spot-secondary">Fetching...</span>12+ <span id="refresh-indicator" class="text-sm text-spot-secondary" style="display:none">Refreshing feeds...</span>
11 </div>13 </div>
12 </div>14 </div>
15+<script>
16+function gleanRefreshStart(){document.getElementById('refresh-indicator').style.display='inline';document.getElementById('refresh-btn').disabled=true}
17+function gleanRefreshPoll(){setTimeout(function(){htmx.ajax('GET','/feeds/list',{target:'#feed-list',swap:'innerHTML'});document.getElementById('refresh-indicator').style.display='none';document.getElementById('refresh-btn').disabled=false},5000)}
18+</script>
13 <p class="text-sm text-spot-secondary mb-6">Manage your RSS and Atom subscriptions.</p>19 <p class="text-sm text-spot-secondary mb-6">Manage your RSS and Atom subscriptions.</p>
14 20
15 {{template "dead-feeds.html" (dict "DeadFeeds" .DeadFeeds "CSRFToken" .CSRFToken)}}21 {{template "dead-feeds.html" (dict "DeadFeeds" .DeadFeeds "CSRFToken" .CSRFToken)}}