feat: cron-based scheduler with auto-download after rescan

- Replace asyncio sleep loop with APScheduler AsyncIOScheduler + CronTrigger
- Add schedule_time (HH:MM), schedule_days (days of week), auto_download_after_rescan fields to SchedulerConfig
- Add _auto_download_missing() to queue missing episodes after rescan
- Reload config live via reload_config(SchedulerConfig) without restart
- Update GET/POST /api/scheduler/config to return {success, config, status} envelope
- Add day-of-week pill toggles to Settings -> Scheduler section in UI
- Update JS loadSchedulerConfig / saveSchedulerConfig for new API shape
- Add 29 unit tests for SchedulerConfig model, 18 unit tests for SchedulerService
- Rewrite 23 endpoint tests and 36 integration tests for APScheduler behaviour
- Coverage: 96% api/scheduler, 95% scheduler_service, 90% total (>= 80% threshold)
- Update docs: API.md, CONFIGURATION.md, features.md, CHANGELOG.md
This commit is contained in:
2026-02-21 08:56:17 +01:00
parent ac7e15e1eb
commit 0265ae2a70
15 changed files with 1923 additions and 1628 deletions

View File

@@ -228,3 +228,122 @@
font-size: var(--font-size-title);
}
}
/* ============================================================
Scheduler day-of-week toggle pills
============================================================ */
.scheduler-days-container {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.scheduler-day-toggle-label {
display: inline-flex;
align-items: center;
cursor: pointer;
user-select: none;
}
/* Hide the raw checkbox visually */
.scheduler-day-toggle-label .scheduler-day-checkbox {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
/* Pill styling */
.scheduler-day-label {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.6rem;
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-xl);
font-size: var(--font-size-caption);
font-weight: 600;
color: var(--color-text-secondary);
background-color: var(--color-bg-secondary);
transition: background-color var(--transition-duration) var(--transition-easing),
color var(--transition-duration) var(--transition-easing),
border-color var(--transition-duration) var(--transition-easing);
cursor: pointer;
}
/* Checked state filled accent */
.scheduler-day-checkbox:checked + .scheduler-day-label {
background-color: var(--color-accent);
border-color: var(--color-accent);
color: #ffffff;
}
/* Hover for unchecked */
.scheduler-day-toggle-label:hover .scheduler-day-label {
border-color: var(--color-accent);
color: var(--color-accent);
}
/* Hover for checked */
.scheduler-day-toggle-label:hover .scheduler-day-checkbox:checked + .scheduler-day-label {
background-color: var(--color-accent-hover);
border-color: var(--color-accent-hover);
color: #ffffff;
}
/* Dark theme overrides */
[data-theme="dark"] .scheduler-day-label {
border-color: var(--color-border-dark);
color: var(--color-text-secondary-dark);
background-color: var(--color-bg-secondary-dark);
}
[data-theme="dark"] .scheduler-day-checkbox:checked + .scheduler-day-label {
background-color: var(--color-accent-dark);
border-color: var(--color-accent-dark);
color: var(--color-bg-primary-dark);
}
[data-theme="dark"] .scheduler-day-toggle-label:hover .scheduler-day-label {
border-color: var(--color-accent-dark);
color: var(--color-accent-dark);
}
/* Next run display */
#scheduler-next-run {
font-style: italic;
font-size: var(--font-size-caption);
color: var(--color-text-tertiary);
}
[data-theme="dark"] #scheduler-next-run {
color: var(--color-text-tertiary-dark);
}
/* Advanced/collapsible section */
.config-advanced {
font-size: var(--font-size-caption);
color: var(--color-text-secondary);
margin-top: var(--spacing-sm);
}
.config-advanced summary {
cursor: pointer;
padding: var(--spacing-xs) 0;
font-weight: 500;
}
/* Responsive: wrap day pills to 2 rows on mobile */
@media (max-width: 480px) {
.scheduler-days-container {
gap: var(--spacing-xs);
}
.scheduler-day-label {
min-width: 2.2rem;
padding: var(--spacing-xs);
}
}