Files
Aniworld/docs/instructions.md

386 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Aniworld Web Application Development Instructions
This document provides detailed tasks for AI agents to implement a modern web application for the Aniworld anime download manager. All tasks should follow the coding guidelines specified in the project's copilot instructions.
## Project Overview
The goal is to create a FastAPI-based web application that provides a modern interface for the existing Aniworld anime download functionality. The core anime logic should remain in `SeriesApp.py` while the web layer provides REST API endpoints and a responsive UI.
## Architecture Principles
- **Single Responsibility**: Each file/class has one clear purpose
- **Dependency Injection**: Use FastAPI's dependency system
- **Clean Separation**: Web layer calls core logic, never the reverse
- **File Size Limit**: Maximum 500 lines per file
- **Type Hints**: Use comprehensive type annotations
- **Error Handling**: Proper exception handling and logging
## Additional Implementation Guidelines
### Code Style and Standards
- **Type Hints**: Use comprehensive type annotations throughout all modules
- **Docstrings**: Follow PEP 257 for function and class documentation
- **Error Handling**: Implement custom exception classes with meaningful messages
- **Logging**: Use structured logging with appropriate log levels
- **Security**: Validate all inputs and sanitize outputs
- **Performance**: Use async/await patterns for I/O operations
## 📞 Escalation
If you encounter:
- Architecture issues requiring design decisions
- Tests that conflict with documented requirements
- Breaking changes needed
- Unclear requirements or expectations
**Document the issue and escalate rather than guessing.**
---
## <20> Credentials
**Admin Login:**
- Username: `admin`
- Password: `Hallo123!`
---
## <20>📚 Helpful Commands
```bash
# Run all tests
conda run -n AniWorld python -m pytest tests/ -v --tb=short
# Run specific test file
conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py -v
# Run specific test class
conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py::TestWebSocketService -v
# Run specific test
conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py::TestWebSocketService::test_broadcast_download_progress -v
# Run with extra verbosity
conda run -n AniWorld python -m pytest tests/ -vv
# Run with full traceback
conda run -n AniWorld python -m pytest tests/ -v --tb=long
# Run and stop at first failure
conda run -n AniWorld python -m pytest tests/ -v -x
# Run tests matching pattern
conda run -n AniWorld python -m pytest tests/ -v -k "auth"
# Show all print statements
conda run -n AniWorld python -m pytest tests/ -v -s
#Run app
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload
```
---
## Implementation Notes
1. **Incremental Development**: Implement features incrementally, testing each component thoroughly before moving to the next
2. **Code Review**: Review all generated code for adherence to project standards
3. **Documentation**: Document all public APIs and complex logic
4. **Testing**: Maintain test coverage above 80% for all new code
5. **Performance**: Profile and optimize critical paths, especially download and streaming operations
6. **Security**: Regular security audits and dependency updates
7. **Monitoring**: Implement comprehensive monitoring and alerting
8. **Maintenance**: Plan for regular maintenance and updates
---
## Task Completion Checklist
For each task completed:
- [ ] Implementation follows coding standards
- [ ] Unit tests written and passing
- [ ] Integration tests passing
- [ ] Documentation updated
- [ ] Error handling implemented
- [ ] Logging added
- [ ] Security considerations addressed
- [ ] Performance validated
- [ ] Code reviewed
- [ ] Task marked as complete in instructions.md
- [ ] Infrastructure.md updated and other docs
- [ ] Changes committed to git; keep your messages in git short and clear
- [ ] Take the next task
---
## TODO List:
### Task 1: Add APScheduler Dependency ✅
- [x] **1.1** Add `APScheduler>=3.10.4` to `requirements.txt`
- [x] **1.2** Verify installation: `conda run -n AniWorld pip install APScheduler>=3.10.4`
- [x] **1.3** Verify import works: `conda run -n AniWorld python -c "from apscheduler.schedulers.asyncio import AsyncIOScheduler; from apscheduler.triggers.cron import CronTrigger; print('OK')"`
---
### Task 2: Extend SchedulerConfig Model ✅
**File:** `src/server/models/config.py`
The existing `SchedulerConfig` has `enabled: bool` and `interval_minutes: int`. Extend it with cron-based fields.
- [x] **2.1** Add field `schedule_time: str = "03:00"` — 24h HH:MM format for the daily run time
- [x] **2.2** Add field `schedule_days: List[str]` with default `["mon","tue","wed","thu","fri","sat","sun"]` — lowercase 3-letter day abbreviations
- [x] **2.3** Add field `auto_download_after_rescan: bool = False` — whether to auto-queue and start downloads of all missing episodes after a scheduled rescan completes
- [x] **2.4** Add a `field_validator` for `schedule_time` that validates HH:MM format (00:0023:59). Raise `ValueError` with a clear message on invalid input
- [x] **2.5** Add a `field_validator` for `schedule_days` that validates each entry is one of `["mon","tue","wed","thu","fri","sat","sun"]`. Raise `ValueError` on invalid entries. Empty list is allowed (means scheduler won't run)
- [x] **2.6** Keep `interval_minutes` field for backward compatibility but it will no longer be used when `schedule_time` is set. Add a docstring noting this deprecation
- [x] **2.7** Update `data/config.json` default to include the new fields with their defaults
**Verification:**
- [x] **2.8** Run: `conda run -n AniWorld python -c "from src.server.models.config import SchedulerConfig; c = SchedulerConfig(); print(c.schedule_time, c.schedule_days, c.auto_download_after_rescan)"`
- [x] **2.9** Verify invalid time raises error: `SchedulerConfig(schedule_time='25:00')` should raise `ValidationError`
- [x] **2.10** Verify invalid day raises error: `SchedulerConfig(schedule_days=['monday'])` should raise `ValidationError`
---
### Task 3: Write Unit Tests for SchedulerConfig Model ✅
**File:** `tests/unit/test_scheduler_config_model.py` (new file)
- [x] **3.1** Test default values: `SchedulerConfig()` creates instance with `schedule_time="03:00"`, `schedule_days` = all 7 days, `auto_download_after_rescan=False`
- [x] **3.2** Test valid `schedule_time` values: `"00:00"`, `"03:00"`, `"12:30"`, `"23:59"`
- [x] **3.3** Test invalid `schedule_time` values: `"25:00"`, `"3pm"`, `""`, `"3:00pm"`, `"24:00"`, `"-1:00"` — all must raise `ValidationError`
- [x] **3.4** Test valid `schedule_days` values: `["mon"]`, `["mon","fri"]`, all 7 days, empty list `[]`
- [x] **3.5** Test invalid `schedule_days` values: `["monday"]`, `["xyz"]`, `["Mon"]` (case-sensitive), `[""]` — all must raise `ValidationError`
- [x] **3.6** Test `auto_download_after_rescan` accepts `True` and `False`
- [x] **3.7** Test backward compatibility: creating `SchedulerConfig(enabled=True, interval_minutes=30)` without new fields uses defaults for `schedule_time`, `schedule_days`, `auto_download_after_rescan`
- [x] **3.8** Test serialization roundtrip: `config.model_dump()` then `SchedulerConfig(**dumped)` produces identical config
**Verification:**
- [x] **3.9** Run: `conda run -n AniWorld python -m pytest tests/unit/test_scheduler_config_model.py -v` — all tests pass
---
### Task 4: Rewrite SchedulerService to Use APScheduler with CronTrigger ✅
**File:** `src/server/services/scheduler_service.py`
The existing service used a custom `asyncio.sleep()` loop in `_scheduler_loop()`. Replaced with APScheduler's `AsyncIOScheduler` and `CronTrigger`.
- [x] **4.1** Add imports: `from apscheduler.schedulers.asyncio import AsyncIOScheduler` and `from apscheduler.triggers.cron import CronTrigger`
- [x] **4.2** Add a private method `_build_cron_trigger(self) -> CronTrigger` that converts `self._config.schedule_time` (HH:MM) and `self._config.schedule_days` (list of day abbreviations) into a `CronTrigger(hour=H, minute=M, day_of_week='mon,wed,fri')`. If `schedule_days` is empty, return `None` (no trigger = don't schedule)
- [x] **4.3** Refactor `start()`: instead of creating an `asyncio.Task` with `_scheduler_loop`, create an `AsyncIOScheduler` instance, add a job using the `CronTrigger` from `_build_cron_trigger()`, and call `scheduler.start()`. Store the scheduler as `self._scheduler`. If `_build_cron_trigger()` returns `None`, log a warning and don't add a job. Set `self._is_running = True`
- [x] **4.4** Refactor `stop()`: call `self._scheduler.shutdown(wait=False)` instead of cancelling an asyncio task. Set `self._is_running = False`
- [x] **4.5** Add method `reload_config(self, config: SchedulerConfig) -> None`: update `self._config`, then if scheduler is running, reschedule the job using `self._scheduler.reschedule_job()` with the new cron trigger. If trigger is `None`, remove the job. If scheduler is not running and config is enabled, call `start()`
- [x] **4.6** Remove or deprecate `_scheduler_loop()` method entirely
- [x] **4.7** Update `get_status()` to return `next_run_time` obtained from `self._scheduler.get_job('scheduled_rescan').next_run_time` (handle case where job doesn't exist). Also include `schedule_time`, `schedule_days`, and `auto_download_after_rescan` in status dict
- [x] **4.8** The job function should be `_perform_rescan()` (existing method) — keep it as the scheduled task. It is already async
- [x] **4.9** Add structured logging: log when scheduler starts, stops, reschedules, and when cron trigger is built (include the cron expression in the log)
- [x] **4.10** Ensure the file stays under 500 lines. If it exceeds, extract helper functions
**Verification:**
- [x] **4.11** Start the app: `conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000`. Check logs for scheduler startup messages
- [x] **4.12** Verify no import errors or startup crashes
---
### Task 5: Implement Auto-Download After Rescan Logic ✅
**File:** `src/server/services/scheduler_service.py`
- [x] **5.1** At the end of `_perform_rescan()`, after the scan completes successfully, check `self._config.auto_download_after_rescan`
- [x] **5.2** If enabled: call `self._anime_service.get_all_anime()` to get all series. For each series that has missing episodes (non-empty `missing_episodes` list), call `self._download_service.add_to_queue()` for each missing episode.
- [x] **5.3** After all episodes are queued, call `self._download_service.start_download()` (or equivalent method from the existing download service) to begin processing the queue
- [x] **5.4** Broadcast a WebSocket event `auto_download_started` with `{"queued_count": N}` where N is the number of episodes queued. Use the existing `_broadcast()` helper method
- [x] **5.5** Wrap the auto-download logic in try/except. On failure, broadcast `auto_download_error` with `{"error": str(e)}` and log the error. Do NOT let auto-download failures crash the scheduler
- [x] **5.6** If `auto_download_after_rescan` is `False`, skip the auto-download logic entirely (just log that auto-download is disabled)
- [x] **5.7** Add structured logging for: auto-download started, number of episodes queued, auto-download completed, auto-download failed
**Verification:**
- [x] **5.8** Read `src/server/services/download_service.py` to confirm the exact method names and signatures used for queueing and starting downloads. Use those exact method names
---
### Task 6: Update Scheduler API Endpoints ✅
**File:** `src/server/api/scheduler.py`
- [x] **6.1** Update **GET `/api/scheduler/config`**: Return response that includes the new config fields (`schedule_time`, `schedule_days`, `auto_download_after_rescan`) plus runtime status (`is_running`, `next_run`, `last_run`). The response shape should be `{"success": true, "config": {...all config fields...}, "status": {...runtime fields...}}`
- [x] **6.2** Update **POST `/api/scheduler/config`**: Accept the new fields in the request body. Validate using the updated `SchedulerConfig` model. After saving config via `config_service`, call `scheduler_service.reload_config(new_config)` to apply changes immediately without restart. Return the same enriched response format as GET
- [x] **6.3** Ensure backward compatibility: if a POST request only sends `{"enabled": true}` without new fields, use existing/default values for the omitted fields
- [x] **6.4** Add proper error responses: 422 for validation errors (invalid time/days), 500 for scheduler failures
- [x] **6.5** The existing **POST `/api/scheduler/trigger-rescan`** endpoint should remain unchanged — it triggers an immediate rescan (which will also auto-download if configured)
**Verification:**
- [x] **6.6** Start the app and test with curl:
- `curl -H "Authorization: Bearer <token>" http://127.0.0.1:8000/api/scheduler/config` — should return new fields
- `curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"enabled":true,"schedule_time":"02:00","schedule_days":["mon","wed","fri"],"auto_download_after_rescan":true}' http://127.0.0.1:8000/api/scheduler/config` — should save and return updated config
---
### Task 7: Update Frontend HTML — Scheduler Settings Section ✅
**File:** `src/server/web/templates/index.html`
- [x] **7.1** Keep the existing "Enable Scheduler" checkbox (`scheduler-enabled`)
- [x] **7.2** Replace or supplement the `scheduler-interval` (minutes) input with a **time picker input** (`type="time"`, id `scheduler-time`). Set default value to `03:00`. Add a label: "Run at" or equivalent with `data-text` attribute for i18n
- [x] **7.3** Add **7 day-of-week toggle checkboxes** (MonSun) with ids `scheduler-day-mon` through `scheduler-day-sun`. All checked by default. Display them in a horizontal row. Each checkbox label should have a `data-text` attribute for i18n. Use short labels: "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"
- [x] **7.4** Keep the existing "Auto-download after rescan" checkbox (it already exists in the HTML as `auto-download-checkbox` around line 292). Ensure it maps to `auto_download_after_rescan`
- [x] **7.5** Hide or remove the old `scheduler-interval` minutes input. If keeping for backward compat, move it to a collapsible "Advanced" section
- [x] **7.6** Add a help text below the day checkboxes: "Scheduler runs at the selected time on checked days. Uncheck all to disable scheduling." — with `data-text` for i18n
- [x] **7.7** Add a display area showing "Next scheduled run: <datetime>" that gets populated by JS (id `scheduler-next-run`)
- [x] **7.8** Ensure all new elements work in both dark and light themes (use existing CSS classes)
**Verification:**
- [ ] **7.9** Start the app, open the settings modal, verify all new UI elements render correctly in both dark and light mode
- [ ] **7.10** Verify the day-of-week checkboxes are displayed horizontally and are all checked by default
---
### Task 8: Update Frontend CSS — Day-of-Week Toggle Styling
**File:** `src/server/web/static/css/styles.css`
> ⚠️ **NOT YET DONE** — The HTML elements exist but CSS classes `.scheduler-days-container`, `.scheduler-day-toggle`, `.scheduler-day-label` are not defined in `styles.css`.
- [ ] **8.1** Add styles for `.scheduler-days-container`: horizontal flexbox layout, gap between items, wrapping on small screens
- [ ] **8.2** Add styles for `.scheduler-day-toggle` checkboxes: styled as pill/button toggles. Unchecked = outline/muted, Checked = accent/filled. Use existing theme CSS variables for colors
- [ ] **8.3** Add styles for `.scheduler-day-label`: centered text, pointer cursor, appropriate padding
- [ ] **8.4** Ensure dark mode support using existing dark mode CSS variables/selectors (check existing dark mode patterns in the file)
- [ ] **8.5** Ensure responsive layout: on mobile, days should wrap to 2 rows if needed
- [ ] **8.6** Style the `#scheduler-next-run` display: muted text, smaller font size, italic
**Verification:**
- [ ] **8.7** Test in browser: verify day toggles look like toggle buttons, dark/light mode both work, responsive on narrow viewport
---
### Task 9: Update Frontend JavaScript — Scheduler Config Load/Save ✅
**File:** `src/server/web/static/js/app.js`
- [x] **9.1** Update `loadSchedulerConfig()` — reads `config.schedule_time`, `config.schedule_days`, `config.auto_download_after_rescan`, and `status.next_run`
- [x] **9.2** Update `saveSchedulerConfig()` — sends `schedule_time`, `schedule_days`, `auto_download_after_rescan`, and `enabled` to POST `/api/scheduler/config`
- [x] **9.3** Update `toggleSchedulerTimeInput()` — toggles visibility of day checkboxes and next-run display when scheduler is enabled/disabled
- [x] **9.4** Handle WebSocket events `auto_download_started` and `auto_download_error` with toast notifications
**Verification:**
- [ ] **9.5** Start the app, open settings, change time to `02:00`, uncheck "Sat" and "Sun", enable auto-download, save. Reload the page and verify values persist
- [ ] **9.6** Check browser console for any JS errors during load/save
- [ ] **9.7** Verify the "Next scheduled run" display updates after saving
---
### Task 10: Update Config Service for Backward Compatibility ✅
**File:** `src/server/services/config_service.py`
- [x] **10.1** Ensure `load_config()` handles old `config.json` files that don't have `schedule_time`, `schedule_days`, or `auto_download_after_rescan` — Pydantic defaults handle this automatically
- [x] **10.2** Ensure `save_config()` writes the new fields to disk
- [x] **10.3** Pydantic model defaults handle migration automatically; no explicit migration step required
**Verification:**
- [ ] **10.4** Delete `schedule_time`, `schedule_days`, and `auto_download_after_rescan` from `data/config.json`, restart the app, verify defaults are used and the app doesn't crash
- [ ] **10.5** Verify the config is saved with the new fields after any settings change
---
### Task 11: Update FastAPI Lifespan for APScheduler ✅
**File:** `src/server/fastapi_app.py`
- [x] **11.1** Verify that the `lifespan` function calls `scheduler_service.start()` on startup — initializes the APScheduler. Confirmed working
- [x] **11.2** Verify that `scheduler_service.stop()` is called on shutdown — calls `scheduler.shutdown()`. Confirmed working
- [x] **11.3** Ensure no duplicate scheduler instances are created (singleton pattern in `get_scheduler_service()` prevents this)
**Verification:**
- [ ] **11.4** Start and stop the app, verify no errors in logs about scheduler startup/shutdown
- [ ] **11.5** Verify logs show "Scheduler started with cron trigger: ..." or similar
---
### Task 12: Write Unit Tests for SchedulerService (APScheduler + Auto-Download) ✅
**File:** `tests/unit/test_scheduler_service.py`
- [x] **12.1** Test `_build_cron_trigger()` with various input combinations
- [x] **12.2** Test `start()` — mocks `AsyncIOScheduler`, verifies `add_job()` and `scheduler.start()` called
- [x] **12.3** Test `start()` with empty `schedule_days` — no job added, warning logged
- [x] **12.4** Test `stop()` — verifies `scheduler.shutdown(wait=False)` called
- [x] **12.5** Test `reload_config()` — verifies `reschedule_job()` called with new CronTrigger
- [x] **12.6** Test `reload_config()` with empty `schedule_days` — verifies job removed
- [x] **12.7** Test `_perform_rescan()` with `auto_download_after_rescan=True`
- [x] **12.8** Test `_perform_rescan()` with `auto_download_after_rescan=False`
- [x] **12.9** Test `_perform_rescan()` auto-download error handling
- [x] **12.10** Test `get_status()` returns correct fields
- [x] **12.11** Removed tests for old `_scheduler_loop()` method
- [x] **12.12** File stays under 500 lines
**Verification:**
- [ ] **12.13** Run: `conda run -n AniWorld python -m pytest tests/unit/test_scheduler_service.py -v --tb=short` — all tests pass
- [ ] **12.14** Run: `conda run -n AniWorld python -m pytest tests/unit/test_scheduler_config_model.py -v --tb=short` — all tests pass
- [ ] **12.15** Run coverage: `conda run -n AniWorld python -m pytest tests/unit/test_scheduler_service.py tests/unit/test_scheduler_config_model.py --cov=src/server/services/scheduler_service --cov=src/server/models/config --cov-report=term-missing` — target ≥80% on both modules
---
### Task 13: Write/Update API Endpoint Tests ✅
**File:** `tests/api/test_scheduler_endpoints.py`
- [x] **13.1** Test GET `/api/scheduler/config` returns response with new fields
- [x] **13.2** Test POST `/api/scheduler/config` with valid new fields saves correctly
- [x] **13.3** Test POST with invalid `schedule_time` (`"25:00"`) returns 422 validation error
- [x] **13.4** Test POST with invalid `schedule_days` (`["monday"]`) returns 422 validation error
- [x] **13.5** Test POST with only `{"enabled": true}` (backward compat) — defaults used
- [x] **13.6** Test POST with `schedule_days: []` — valid, scheduler has no scheduled jobs
- [x] **13.7** Test trigger-rescan endpoint still works correctly
**Verification:**
- [ ] **13.8** Run: `conda run -n AniWorld python -m pytest tests/api/test_scheduler_endpoints.py -v --tb=short` — all tests pass
---
### Task 14: Write/Update Integration Tests ✅
**File:** `tests/integration/test_scheduler_workflow.py`
- [x] **14.1** Test full workflow: Enable scheduler with `schedule_time="02:00"` and `schedule_days=["mon","fri"]`
- [x] **14.2** Test config change workflow: Change `schedule_time` via API → job rescheduled
- [x] **14.3** Test disable/re-enable workflow
- [x] **14.4** Test auto-download integration with `auto_download_after_rescan=True`
- [x] **14.5** Test auto-download disabled scenario
**Verification:**
- [ ] **14.6** Run: `conda run -n AniWorld python -m pytest tests/integration/test_scheduler_workflow.py -v --tb=short` — all tests pass
---
### Task 15: Run Full Test Suite and Verify Coverage
- [ ] **15.1** Run all tests: `conda run -n AniWorld python -m pytest tests/ -v --tb=short` — all tests pass, no regressions
- [ ] **15.2** Run coverage for scheduler-related code: `conda run -n AniWorld python -m pytest tests/ --cov=src/server/services/scheduler_service --cov=src/server/models/config --cov=src/server/api/scheduler --cov-report=term-missing` — all three modules ≥80%
- [ ] **15.3** Fix any failing tests from other test files that may have been affected by the config model changes
- [ ] **15.4** Run: `conda run -n AniWorld python -m pytest tests/ -v -x` — stop at first failure, fix, repeat until all green
---
### Task 16: Update Documentation ✅
- [x] **16.1** Updated `docs/features.md`: Cron-based scheduling, auto-download after rescan, and time + day-of-week configuration documented
- [x] **16.2** Updated `docs/CONFIGURATION.md`: `scheduler.schedule_time`, `scheduler.schedule_days`, `scheduler.auto_download_after_rescan` config fields documented with table and examples
- [x] **16.3** Updated `docs/API.md`: GET/POST `/api/scheduler/config` request/response schemas documented with new fields
- [x] **16.4** Updated `docs/instructions.md`: Tasks 116 marked as completed
- [x] **16.5** Updated `docs/CHANGELOG.md`: Cron scheduler feature entry added under `[Unreleased]`
---
### Task 17: Final Manual Verification
- [ ] **17.1** Start the app: `conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload`
- [ ] **17.2** Log in with admin credentials (`admin` / `Hallo123!`)
- [ ] **17.3** Open settings modal → Scheduler section. Verify: time picker shows `03:00`, all 7 days are checked, auto-download is unchecked
- [ ] **17.4** Change time to `02:00`, uncheck Sat and Sun, enable auto-download, save. Verify toast notification confirms save
- [ ] **17.5** Reload the page, reopen settings. Verify saved values persist
- [ ] **17.6** Check terminal logs for: "Scheduler rescheduled with cron trigger: ..." message
- [ ] **17.7** Trigger manual rescan. If auto-download is enabled and there are missing episodes, verify episodes appear in download queue
- [ ] **17.8** Disable scheduler entirely, save. Verify logs show scheduler stopped
- [ ] **17.9** Re-enable scheduler, save. Verify logs show scheduler started with correct cron expression
- [ ] **17.10** Test dark mode and light mode — all scheduler UI elements render correctly
- [ ] **17.11** Test on narrow viewport (mobile) — day checkboxes wrap properly (note: CSS Task 8 must be done first)
---
### Task 18: Commit and Clean Up
- [ ] **18.1** Review all changed files for adherence to architecture principles (single responsibility, <500 lines, type hints, docstrings)
- [ ] **18.2** Run linter: `conda run -n AniWorld python -m flake8 src/server/services/scheduler_service.py src/server/models/config.py src/server/api/scheduler.py` — no critical issues
- [x] **18.3** Commit with message: `feat: cron-based scheduler with auto-download after rescan`
- [ ] **18.4** Mark this feature as complete in `docs/instructions.md` TODO list