feat: add reading_status_push auto-refresh schedule per library

- Migration 0059: reading_status_push_mode / last / next columns on libraries
- API: update_reading_status_provider accepts push_mode and calculates next_push_at
- job_poller: handles reading_status_push pending jobs
- Indexer scheduler: check_and_schedule_reading_status_push every minute
- Backoffice: schedule select in library settings modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 12:46:48 +01:00
parent 57ff1888eb
commit f3960666fa
11 changed files with 166 additions and 13 deletions

View File

@@ -147,6 +147,7 @@ export default async function LibrariesPage() {
fallbackMetadataProvider={lib.fallback_metadata_provider}
metadataRefreshMode={lib.metadata_refresh_mode}
readingStatusProvider={lib.reading_status_provider}
readingStatusPushMode={lib.reading_status_push_mode}
/>
<form>
<input type="hidden" name="id" value={lib.id} />

View File

@@ -15,6 +15,7 @@ interface LibraryActionsProps {
fallbackMetadataProvider: string | null;
metadataRefreshMode: string;
readingStatusProvider: string | null;
readingStatusPushMode: string;
onUpdate?: () => void;
}
@@ -27,6 +28,7 @@ export function LibraryActions({
fallbackMetadataProvider,
metadataRefreshMode,
readingStatusProvider,
readingStatusPushMode,
}: LibraryActionsProps) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
@@ -43,6 +45,7 @@ export function LibraryActions({
const newFallbackProvider = (formData.get("fallback_metadata_provider") as string) || null;
const newMetadataRefreshMode = formData.get("metadata_refresh_mode") as string;
const newReadingStatusProvider = (formData.get("reading_status_provider") as string) || null;
const newReadingStatusPushMode = (formData.get("reading_status_push_mode") as string) || "manual";
try {
const [response] = await Promise.all([
@@ -64,7 +67,10 @@ export function LibraryActions({
fetch(`/api/libraries/${libraryId}/reading-status-provider`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reading_status_provider: newReadingStatusProvider }),
body: JSON.stringify({
reading_status_provider: newReadingStatusProvider,
reading_status_push_mode: newReadingStatusPushMode,
}),
}),
]);
@@ -289,6 +295,22 @@ export function LibraryActions({
</div>
<p className="text-xs text-muted-foreground mt-1.5">{t("libraryActions.readingStatusProviderDesc")}</p>
</div>
<div>
<div className="flex items-center justify-between gap-4">
<label className="text-sm font-medium text-foreground">{t("libraryActions.readingStatusPushSchedule")}</label>
<select
name="reading_status_push_mode"
defaultValue={readingStatusPushMode}
className="text-sm border border-border rounded-lg px-3 py-1.5 bg-background min-w-[160px] shrink-0"
>
<option value="manual">{t("monitoring.manual")}</option>
<option value="hourly">{t("monitoring.hourly")}</option>
<option value="daily">{t("monitoring.daily")}</option>
<option value="weekly">{t("monitoring.weekly")}</option>
</select>
</div>
<p className="text-xs text-muted-foreground mt-1.5">{t("libraryActions.readingStatusPushScheduleDesc")}</p>
</div>
</div>
{saveError && (

View File

@@ -15,6 +15,8 @@ export type LibraryDto = {
series_count: number;
thumbnail_book_ids: string[];
reading_status_provider: string | null;
reading_status_push_mode: string;
next_reading_status_push_at: string | null;
};
export type IndexJobDto = {

View File

@@ -200,6 +200,8 @@ const en: Record<TranslationKey, string> = {
"libraryActions.sectionReadingStatus": "Reading Status",
"libraryActions.readingStatusProvider": "Reading Status Provider",
"libraryActions.readingStatusProviderDesc": "Syncs reading states (read / reading / planned) with an external service",
"libraryActions.readingStatusPushSchedule": "Auto-push schedule",
"libraryActions.readingStatusPushScheduleDesc": "Automatically push reading progress to the provider on a schedule",
// Reading status modal
"readingStatus.button": "Reading status",

View File

@@ -198,6 +198,8 @@ const fr = {
"libraryActions.sectionReadingStatus": "État de lecture",
"libraryActions.readingStatusProvider": "Provider d'état de lecture",
"libraryActions.readingStatusProviderDesc": "Synchronise les états de lecture (lu / en cours / planifié) avec un service externe",
"libraryActions.readingStatusPushSchedule": "Synchronisation automatique",
"libraryActions.readingStatusPushScheduleDesc": "Pousse automatiquement la progression de lecture vers le provider selon un calendrier",
// Reading status modal
"readingStatus.button": "État de lecture",