Exclude feeds from retry loop after repeated errorsUnverified
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 | ||
| 77 | 77 | last_fetched_at, last_error, subscriber_count, etag, last_modified, |
| 78 | 78 | fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url |
| 79 | 79 | 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 | 81 | ORDER BY last_fetched_at ASC NULLS FIRST |
| 82 | 82 | LIMIT ? |
| 83 | 83 | `, 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_url | 78 | fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url |
| 79 | FROM feeds | 79 | 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 FIRST | 81 | 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) { | ||
| 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 | 362 | func (s *Server) handleDiscoverFeedURL(w http.ResponseWriter, r *http.Request) { |
| 326 | 363 | siteURL := r.URL.Query().Get("url") |
| 327 | 364 | 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() { | ||
| 158 | 158 | r.Post("/opml/upload", s.handleOPMLUpload) |
| 159 | 159 | r.Get("/opml/download", s.handleOPMLDownload) |
| 160 | 160 | r.Post("/refresh", s.handleRefreshFeeds) |
| 161 | + r.Post("/retry", s.handleRetryFeed) | |
| 161 | 162 | r.Get("/list", s.handleFeedList) |
| 162 | 163 | r.Get("/discover-url", s.handleDiscoverFeedURL) |
| 163 | 164 | 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 @@ | ||
| 12 | 12 | </div> |
| 13 | 13 | <p class="text-sm text-spot-secondary mb-6">Manage your RSS and Atom subscriptions.</p> |
| 14 | 14 | |
| 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)}} | |
| 39 | 16 | |
| 40 | 17 | {{if .FeedRecommendations}} |
| 41 | 18 | <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}} | ||