77e9859 Rewire the Subscriptions → Downloads MaintenanceMenu to the downloads pipeline. "Retry failed" now bulk-retries failing sources (same loop as the existing RETRY ALL — sourcesStore.checkNow per source); "Force recovery sweep" triggers recover_stalled_download_events on demand via a new POST /api/downloads/recover-stalled endpoint. The menu was wired to the filesystem-import retry endpoint and queued nothing visible from the Downloads view.
d28db32 Pin SourceConfig.timeout default to 900s (matches Celery download_source.soft_time_limit) instead of the inverted 3600s. gallery-dl now raises subprocess.TimeoutExpiredinside Celery's kill window, so the existing clean-error branch (gallery_dl.py:635) actually fires — hangs flip to error_type='timeout' in ~15 min instead of ~40 min via SIGKILL + recovery sweep. Per-source config_overrides.timeout still bumps for legitimately long syncs.
61ce1ce Platform-wide cooldown on ErrorType.RATE_LIMITED: _update_source_health stamps AppSetting("platform_cooldown:<platform>", expires_iso) with a 15-min cooldown. select_due_sources excludes every source whose platform is in active cooldown; scheduler_status surfaces them as platform_cooldowns: {platform: iso}. Burst-prevention for multi-worker setups (N due Patreon sources no longer all retry into the same rate limit at once). INSERT ... ON CONFLICT DO UPDATE so concurrent workers don't race into rolled-back transactions.
ff9e96e Updated two pinned key-set assertions (test_schedule_status_shape, test_summary_returns_rollup_shape) for the new platform_cooldowns field.
No alembic migrations. No new env vars. New backend surface: POST /api/downloads/recover-stalled (202 + {queued: true}).
Test plan
After :latest rebuilds and compose restarts:
Subscriptions → Downloads → Maintenance dropdown → Retry failed → toast N queued, M already running; rerun-buttons spin on source rows.
Same dropdown → Force recovery sweep → toast "Recovery sweep queued — refreshing in a few seconds"; failing rollup auto-refreshes after 4s.
Retry a failing source whose download still fails → flips to error in ~15 min with error_type: timeout (or rate_limited), not ~30–40 min with stranded by recovery sweep.
If multiple sources on the same platform are due and one hits a rate limit, subsequent ticks skip the rest of that platform for 15 min — visible at /api/system/activity/summary under scheduler.platform_cooldowns.
## Summary
- **`77e9859`** Rewire the Subscriptions → Downloads MaintenanceMenu to the downloads pipeline. "Retry failed" now bulk-retries failing sources (same loop as the existing RETRY ALL — `sourcesStore.checkNow` per source); "Force recovery sweep" triggers `recover_stalled_download_events` on demand via a new `POST /api/downloads/recover-stalled` endpoint. The menu was wired to the filesystem-import retry endpoint and queued nothing visible from the Downloads view.
- **`d28db32`** Pin `SourceConfig.timeout` default to **900s** (matches Celery `download_source.soft_time_limit`) instead of the inverted **3600s**. gallery-dl now raises `subprocess.TimeoutExpired` *inside* Celery's kill window, so the existing clean-error branch (`gallery_dl.py:635`) actually fires — hangs flip to `error_type='timeout'` in ~15 min instead of ~40 min via SIGKILL + recovery sweep. Per-source `config_overrides.timeout` still bumps for legitimately long syncs.
- **`61ce1ce`** Platform-wide cooldown on `ErrorType.RATE_LIMITED`: `_update_source_health` stamps `AppSetting("platform_cooldown:<platform>", expires_iso)` with a 15-min cooldown. `select_due_sources` excludes every source whose platform is in active cooldown; `scheduler_status` surfaces them as `platform_cooldowns: {platform: iso}`. Burst-prevention for multi-worker setups (N due Patreon sources no longer all retry into the same rate limit at once). `INSERT ... ON CONFLICT DO UPDATE` so concurrent workers don't race into rolled-back transactions.
- **`ff9e96e`** Updated two pinned key-set assertions (`test_schedule_status_shape`, `test_summary_returns_rollup_shape`) for the new `platform_cooldowns` field.
No alembic migrations. No new env vars. New backend surface: `POST /api/downloads/recover-stalled` (202 + `{queued: true}`).
## Test plan
After `:latest` rebuilds and compose restarts:
- [ ] Subscriptions → Downloads → Maintenance dropdown → **Retry failed** → toast `N queued, M already running`; rerun-buttons spin on source rows.
- [ ] Same dropdown → **Force recovery sweep** → toast "Recovery sweep queued — refreshing in a few seconds"; failing rollup auto-refreshes after 4s.
- [ ] Retry a failing source whose download still fails → flips to `error` in **~15 min** with `error_type: timeout` (or `rate_limited`), not ~30–40 min with `stranded by recovery sweep`.
- [ ] If multiple sources on the same platform are due and one hits a rate limit, subsequent ticks skip the rest of that platform for 15 min — visible at `/api/system/activity/summary` under `scheduler.platform_cooldowns`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
77e9859Rewire the Subscriptions → Downloads MaintenanceMenu to the downloads pipeline. "Retry failed" now bulk-retries failing sources (same loop as the existing RETRY ALL —sourcesStore.checkNowper source); "Force recovery sweep" triggersrecover_stalled_download_eventson demand via a newPOST /api/downloads/recover-stalledendpoint. The menu was wired to the filesystem-import retry endpoint and queued nothing visible from the Downloads view.d28db32PinSourceConfig.timeoutdefault to 900s (matches Celerydownload_source.soft_time_limit) instead of the inverted 3600s. gallery-dl now raisessubprocess.TimeoutExpiredinside Celery's kill window, so the existing clean-error branch (gallery_dl.py:635) actually fires — hangs flip toerror_type='timeout'in ~15 min instead of ~40 min via SIGKILL + recovery sweep. Per-sourceconfig_overrides.timeoutstill bumps for legitimately long syncs.61ce1cePlatform-wide cooldown onErrorType.RATE_LIMITED:_update_source_healthstampsAppSetting("platform_cooldown:<platform>", expires_iso)with a 15-min cooldown.select_due_sourcesexcludes every source whose platform is in active cooldown;scheduler_statussurfaces them asplatform_cooldowns: {platform: iso}. Burst-prevention for multi-worker setups (N due Patreon sources no longer all retry into the same rate limit at once).INSERT ... ON CONFLICT DO UPDATEso concurrent workers don't race into rolled-back transactions.ff9e96eUpdated two pinned key-set assertions (test_schedule_status_shape,test_summary_returns_rollup_shape) for the newplatform_cooldownsfield.No alembic migrations. No new env vars. New backend surface:
POST /api/downloads/recover-stalled(202 +{queued: true}).Test plan
After
:latestrebuilds and compose restarts:N queued, M already running; rerun-buttons spin on source rows.errorin ~15 min witherror_type: timeout(orrate_limited), not ~30–40 min withstranded by recovery sweep./api/system/activity/summaryunderscheduler.platform_cooldowns.🤖 Generated with Claude Code