nandi/gleanpublic⑂ Fork 0
⑂ 6bfb20e
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.

Exclude feeds from retry loop after repeated errorsUnverified

Julien Robert committed 2026-04-21T22:39:17+02:00 Browse files
6bfb20e parent: d1e88c1
modified internal/db/feed.go +1 -1
@@ -77,7 +77,7 @@ func (db *DB) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limi
7777 last_fetched_at, last_error, subscriber_count, etag, last_modified,
7878 fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url
7979 FROM feeds
80- WHERE subscriber_count > 0 AND (last_fetched_at IS NULL OR last_fetched_at <= ?)
80+ WHERE subscriber_count > 0 AND error_count < 25 AND (last_fetched_at IS NULL OR last_fetched_at <= ?)
8181 ORDER BY last_fetched_at ASC NULLS FIRST
8282 LIMIT ?
8383 `, cutoff, limit)
@@ -77,7 +77,7 @@ func (db *DB) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limi
77 last_fetched_at, last_error, subscriber_count, etag, last_modified,77 last_fetched_at, last_error, subscriber_count, etag, last_modified,
78 fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url78 fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url
79 FROM feeds79 FROM feeds
80- WHERE subscriber_count > 0 AND (last_fetched_at IS NULL OR last_fetched_at <= ?)80+ WHERE subscriber_count > 0 AND error_count < 25 AND (last_fetched_at IS NULL OR last_fetched_at <= ?)
81 ORDER BY last_fetched_at ASC NULLS FIRST81 ORDER BY last_fetched_at ASC NULLS FIRST
82 LIMIT ?82 LIMIT ?
83 `, cutoff, limit)83 `, cutoff, limit)
modified internal/server/feeds_handler.go +37 -0
@@ -322,6 +322,43 @@ func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
322322 })
323323 }
324324
325+func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
326+ feedURL := r.FormValue("url")
327+ if feedURL == "" {
328+ http.Error(w, "url required", http.StatusBadRequest)
329+ return
330+ }
331+
332+ f, err := s.db.GetFeed(r.Context(), feedURL)
333+ if err != nil {
334+ http.Error(w, "feed not found", http.StatusNotFound)
335+ return
336+ }
337+
338+ ff := &feed.Feed{
339+ URL: f.FeedURL,
340+ Title: f.Title.String,
341+ SiteURL: f.SiteURL.String,
342+ Description: f.Description.String,
343+ Type: f.FeedType.String,
344+ ETag: f.Etag.String,
345+ LastModified: f.LastModified.String,
346+ }
347+ s.scheduler.FetchFeed(r.Context(), ff)
348+
349+ user := currentUser(r)
350+ deadFeeds, _ := s.db.ListDeadFeeds(r.Context(), user.DID, 7)
351+ if len(deadFeeds) == 0 {
352+ w.Header().Set("Content-Type", "text/html")
353+ w.Write([]byte(""))
354+ return
355+ }
356+
357+ s.render(w, r, "dead-feeds.html", map[string]any{
358+ "DeadFeeds": deadFeeds,
359+ })
360+}
361+
325362 func (s *Server) handleDiscoverFeedURL(w http.ResponseWriter, r *http.Request) {
326363 siteURL := r.URL.Query().Get("url")
327364 if siteURL == "" {
@@ -322,6 +322,43 @@ func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
322 })322 })
323 }323 }
324 324
325+func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
326+ feedURL := r.FormValue("url")
327+ if feedURL == "" {
328+ http.Error(w, "url required", http.StatusBadRequest)
329+ return
330+ }
331+
332+ f, err := s.db.GetFeed(r.Context(), feedURL)
333+ if err != nil {
334+ http.Error(w, "feed not found", http.StatusNotFound)
335+ return
336+ }
337+
338+ ff := &feed.Feed{
339+ URL: f.FeedURL,
340+ Title: f.Title.String,
341+ SiteURL: f.SiteURL.String,
342+ Description: f.Description.String,
343+ Type: f.FeedType.String,
344+ ETag: f.Etag.String,
345+ LastModified: f.LastModified.String,
346+ }
347+ s.scheduler.FetchFeed(r.Context(), ff)
348+
349+ user := currentUser(r)
350+ deadFeeds, _ := s.db.ListDeadFeeds(r.Context(), user.DID, 7)
351+ if len(deadFeeds) == 0 {
352+ w.Header().Set("Content-Type", "text/html")
353+ w.Write([]byte(""))
354+ return
355+ }
356+
357+ s.render(w, r, "dead-feeds.html", map[string]any{
358+ "DeadFeeds": deadFeeds,
359+ })
360+}
361+
325 func (s *Server) handleDiscoverFeedURL(w http.ResponseWriter, r *http.Request) {362 func (s *Server) handleDiscoverFeedURL(w http.ResponseWriter, r *http.Request) {
326 siteURL := r.URL.Query().Get("url")363 siteURL := r.URL.Query().Get("url")
327 if siteURL == "" {364 if siteURL == "" {
modified internal/server/server.go +1 -0
@@ -158,6 +158,7 @@ func (s *Server) setupRoutes() {
158158 r.Post("/opml/upload", s.handleOPMLUpload)
159159 r.Get("/opml/download", s.handleOPMLDownload)
160160 r.Post("/refresh", s.handleRefreshFeeds)
161+ r.Post("/retry", s.handleRetryFeed)
161162 r.Get("/list", s.handleFeedList)
162163 r.Get("/discover-url", s.handleDiscoverFeedURL)
163164 r.Post("/clear", s.handleClearAllSubscriptions)
@@ -158,6 +158,7 @@ func (s *Server) setupRoutes() {
158 r.Post("/opml/upload", s.handleOPMLUpload)158 r.Post("/opml/upload", s.handleOPMLUpload)
159 r.Get("/opml/download", s.handleOPMLDownload)159 r.Get("/opml/download", s.handleOPMLDownload)
160 r.Post("/refresh", s.handleRefreshFeeds)160 r.Post("/refresh", s.handleRefreshFeeds)
161+ r.Post("/retry", s.handleRetryFeed)
161 r.Get("/list", s.handleFeedList)162 r.Get("/list", s.handleFeedList)
162 r.Get("/discover-url", s.handleDiscoverFeedURL)163 r.Get("/discover-url", s.handleDiscoverFeedURL)
163 r.Post("/clear", s.handleClearAllSubscriptions)164 r.Post("/clear", s.handleClearAllSubscriptions)
modified internal/tmpl/feeds.html +1 -24
@@ -12,30 +12,7 @@
1212 </div>
1313 <p class="text-sm text-spot-secondary mb-6">Manage your RSS and Atom subscriptions.</p>
1414
15-{{if .DeadFeeds}}
16-<div id="dead-feeds" class="bg-spot-red/10 border border-spot-red/30 rounded-lg p-4 mb-6">
17- <h3 class="text-sm font-bold text-spot-red mb-2">Feeds with errors ({{len .DeadFeeds}})</h3>
18- <div class="space-y-2">
19- {{range .DeadFeeds}}
20- <div class="dead-feed-item flex items-start justify-between gap-3 text-sm flex-wrap">
21- <div class="min-w-0 flex-1">
22- <span class="text-spot-text truncate block">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.FeedURL}}{{end}}</span>
23- <div class="flex items-center gap-2 mt-1 flex-wrap">
24- <span class="text-spot-red text-xs">{{.ErrorCount}} errors</span>
25- {{if .LastError.Valid}}<span class="text-spot-secondary text-xs truncate max-w-48" title="{{.LastError.String}}">{{.LastError.String}}</span>{{end}}
26- </div>
27- </div>
28- <form hx-delete="/feeds/remove" hx-target="closest .dead-feed-item" hx-swap="outerHTML swap:0.3s"
29- hx-confirm="Unsubscribe from this broken feed?" class="inline shrink-0">
30- {{csrfInput $.CSRFToken}}
31- <input type="hidden" name="url" value="{{.FeedURL}}">
32- <button type="submit" class="text-xs text-spot-secondary hover:text-spot-red transition font-bold uppercase">Unsubscribe</button>
33- </form>
34- </div>
35- {{end}}
36- </div>
37-</div>
38-{{end}}
15+{{template "dead-feeds.html" (dict "DeadFeeds" .DeadFeeds "CSRFToken" .CSRFToken)}}
3916
4017 {{if .FeedRecommendations}}
4118 <div class="mb-6">
@@ -12,30 +12,7 @@
12 </div>12 </div>
13 <p class="text-sm text-spot-secondary mb-6">Manage your RSS and Atom subscriptions.</p>13 <p class="text-sm text-spot-secondary mb-6">Manage your RSS and Atom subscriptions.</p>
14 14
15-{{if .DeadFeeds}}15+{{template "dead-feeds.html" (dict "DeadFeeds" .DeadFeeds "CSRFToken" .CSRFToken)}}
16-<div id="dead-feeds" class="bg-spot-red/10 border border-spot-red/30 rounded-lg p-4 mb-6">
17- <h3 class="text-sm font-bold text-spot-red mb-2">Feeds with errors ({{len .DeadFeeds}})</h3>
18- <div class="space-y-2">
19- {{range .DeadFeeds}}
20- <div class="dead-feed-item flex items-start justify-between gap-3 text-sm flex-wrap">
21- <div class="min-w-0 flex-1">
22- <span class="text-spot-text truncate block">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.FeedURL}}{{end}}</span>
23- <div class="flex items-center gap-2 mt-1 flex-wrap">
24- <span class="text-spot-red text-xs">{{.ErrorCount}} errors</span>
25- {{if .LastError.Valid}}<span class="text-spot-secondary text-xs truncate max-w-48" title="{{.LastError.String}}">{{.LastError.String}}</span>{{end}}
26- </div>
27- </div>
28- <form hx-delete="/feeds/remove" hx-target="closest .dead-feed-item" hx-swap="outerHTML swap:0.3s"
29- hx-confirm="Unsubscribe from this broken feed?" class="inline shrink-0">
30- {{csrfInput $.CSRFToken}}
31- <input type="hidden" name="url" value="{{.FeedURL}}">
32- <button type="submit" class="text-xs text-spot-secondary hover:text-spot-red transition font-bold uppercase">Unsubscribe</button>
33- </form>
34- </div>
35- {{end}}
36- </div>
37-</div>
38-{{end}}
39 16
40 {{if .FeedRecommendations}}17 {{if .FeedRecommendations}}
41 <div class="mb-6">18 <div class="mb-6">
added internal/tmpl/partials/dead-feeds.html +34 -0
new file mode 100644
@@ -0,0 +1,34 @@
1+{{define "dead-feeds.html"}}
2+{{if .DeadFeeds}}
3+<div id="dead-feeds" class="bg-spot-red/10 border border-spot-red/30 rounded-lg p-4 mb-6">
4+ <h3 class="text-sm font-bold text-spot-red mb-2">Feeds with errors ({{len .DeadFeeds}})</h3>
5+ <div class="space-y-2">
6+ {{range .DeadFeeds}}
7+ <div class="dead-feed-item flex items-start justify-between gap-3 text-sm flex-wrap">
8+ <div class="min-w-0 flex-1">
9+ <span class="text-spot-text truncate block">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.FeedURL}}{{end}}</span>
10+ <div class="flex items-center gap-2 mt-1 flex-wrap">
11+ <span class="text-spot-red text-xs">{{.ErrorCount}} errors</span>
12+ {{if .LastError.Valid}}<span class="text-spot-secondary text-xs truncate max-w-48" title="{{.LastError.String}}">{{.LastError.String}}</span>{{end}}
13+ </div>
14+ </div>
15+ <div class="flex items-center gap-3 shrink-0">
16+ <form hx-post="/feeds/retry" hx-target="#dead-feeds" hx-swap="outerHTML" class="inline">
17+ <input type="hidden" name="url" value="{{.FeedURL}}">
18+ <button type="submit" class="text-xs text-spot-secondary hover:text-spot-green transition font-bold uppercase">Retry</button>
19+ </form>
20+ <form hx-delete="/feeds/remove" hx-target="closest .dead-feed-item" hx-swap="outerHTML swap:0.3s"
21+ hx-confirm="Unsubscribe from this broken feed?" class="inline">
22+ {{csrfInput $.CSRFToken}}
23+ <input type="hidden" name="url" value="{{.FeedURL}}">
24+ <button type="submit" class="text-xs text-spot-secondary hover:text-spot-red transition font-bold uppercase">Unsubscribe</button>
25+ </form>
26+ </div>
27+ </div>
28+ {{end}}
29+ </div>
30+</div>
31+{{else}}
32+<span></span>
33+{{end}}
34+{{end}}
new file mode 100644
@@ -0,0 +1,34 @@
1+{{define "dead-feeds.html"}}
2+{{if .DeadFeeds}}
3+<div id="dead-feeds" class="bg-spot-red/10 border border-spot-red/30 rounded-lg p-4 mb-6">
4+ <h3 class="text-sm font-bold text-spot-red mb-2">Feeds with errors ({{len .DeadFeeds}})</h3>
5+ <div class="space-y-2">
6+ {{range .DeadFeeds}}
7+ <div class="dead-feed-item flex items-start justify-between gap-3 text-sm flex-wrap">
8+ <div class="min-w-0 flex-1">
9+ <span class="text-spot-text truncate block">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.FeedURL}}{{end}}</span>
10+ <div class="flex items-center gap-2 mt-1 flex-wrap">
11+ <span class="text-spot-red text-xs">{{.ErrorCount}} errors</span>
12+ {{if .LastError.Valid}}<span class="text-spot-secondary text-xs truncate max-w-48" title="{{.LastError.String}}">{{.LastError.String}}</span>{{end}}
13+ </div>
14+ </div>
15+ <div class="flex items-center gap-3 shrink-0">
16+ <form hx-post="/feeds/retry" hx-target="#dead-feeds" hx-swap="outerHTML" class="inline">
17+ <input type="hidden" name="url" value="{{.FeedURL}}">
18+ <button type="submit" class="text-xs text-spot-secondary hover:text-spot-green transition font-bold uppercase">Retry</button>
19+ </form>
20+ <form hx-delete="/feeds/remove" hx-target="closest .dead-feed-item" hx-swap="outerHTML swap:0.3s"
21+ hx-confirm="Unsubscribe from this broken feed?" class="inline">
22+ {{csrfInput $.CSRFToken}}
23+ <input type="hidden" name="url" value="{{.FeedURL}}">
24+ <button type="submit" class="text-xs text-spot-secondary hover:text-spot-red transition font-bold uppercase">Unsubscribe</button>
25+ </form>
26+ </div>
27+ </div>
28+ {{end}}
29+ </div>
30+</div>
31+{{else}}
32+<span></span>
33+{{end}}
34+{{end}}