Backend:
- src/web/routers/stats.py: filesystem scan via asyncio.to_thread with 30s TTL cache
- src/web/routers/media.py: async thumbnail generation with per-file lock + semaphore(2)
- src/database.py: consolidate get_authors_with_stats (was N+1 favorites lookup) into a single query with EXISTS subquery
- src/web/routers/{media,favorites}.py: optional pagination for subreddits/authors/blacklist-preview/favorites-authors
- src/web/rate_limit.py (new): per-IP+path token bucket dependency
- Rate limits applied to /api/stats, /api/stats/enhanced (30/min), cleanup-blacklist, cleanup-by-type (5/min)
- src/web/auth.py (new): optional HTTP Basic auth, activated when RMC_AUTH_USER and RMC_AUTH_PASS are both set
- src/web/app.py: mount StaticFiles at /static, register auth dependency app-wide
Frontend:
- src/web/static/js/api.js (new): fetchJSON/debounce/throttle helpers on window.api
- index.html: debounced gallery filter onchange handlers (200ms)
- index.html: window eviction at 500 items in galleryItems (keeps DOM lean during long sessions)
- index.html: pause status polling on visibilitychange (no fetches when tab is hidden)
- index.html: loadStats() now throttled to 2s, with force=true used after delete/cleanup
- index.html: parallel batch DELETE with 5 workers + progress UI
- index.html: adapt to new response shape from paginated subreddits/authors endpoints
- Add _safe_resolve helper validating that resolved path stays within DOWNLOADS_DIR
- Apply to get_media_file and get_video_thumbnail
- Reject null bytes and overlong filenames
- Replace bare except in magic-byte fallback with OSError + logging