From aa2456fbf826ec55f38544b3fc2c329600789fa5 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Fri, 13 Mar 2026 08:14:45 -0600 Subject: [PATCH] feat(authors): add auto-enrichment queue, worker safety, and live status --- .../features/settings/MaintenanceSettings.vue | 146 +- .../composables/useAuthorEnrichmentStatus.ts | 48 + packages/types/src/author.ts | 17 + scripts/db/reset.sh | 7 +- server/package.json | 10 +- .../0001_author_enrichment_queue.sql | 17 + ...hor_enrichment_single_processing_guard.sql | 1 + .../src/db/migrations/meta/0001_snapshot.json | 3944 ++++++++++++++++ .../src/db/migrations/meta/0002_snapshot.json | 3960 +++++++++++++++++ server/src/db/migrations/meta/_journal.json | 14 + server/src/db/schema/metadata.ts | 35 +- .../app-settings/app-settings.service.test.ts | 30 + .../app-settings/app-settings.service.ts | 27 + ...author-enrichment-executor.service.test.ts | 171 + .../author-enrichment-executor.service.ts | 150 + ...or-enrichment-orchestrator.service.test.ts | 155 + .../author-enrichment-orchestrator.service.ts | 183 + .../authors/author-enrichment.gateway.test.ts | 47 + .../authors/author-enrichment.gateway.ts | 57 + .../author-enrichment.repository.test.ts | 202 + .../authors/author-enrichment.repository.ts | 189 + .../src/modules/authors/authors.controller.ts | 6 + server/src/modules/authors/authors.module.ts | 29 +- .../authors/authors.repository.test.ts | 43 + .../src/modules/authors/authors.repository.ts | 2 +- .../modules/authors/authors.service.test.ts | 66 +- server/src/modules/authors/authors.service.ts | 43 +- .../author-metadata-fetch.service.test.ts | 71 + .../metadata/author-metadata-fetch.service.ts | 88 +- .../audnexus/audnexus.provider.test.ts | 6 +- .../providers/audnexus/audnexus.provider.ts | 32 +- .../providers/author-metadata-provider.ts | 21 + .../metadata/metadata-events.service.ts | 12 + .../modules/metadata/metadata.module.test.ts | 5 +- .../src/modules/metadata/metadata.module.ts | 5 +- .../modules/metadata/metadata.service.test.ts | 42 + .../src/modules/metadata/metadata.service.ts | 12 + server/src/modules/seed/seed.service.ts | 15 + server/test/app.e2e-spec.ts | 2 + server/vitest.config.ts | 2 +- 40 files changed, 9832 insertions(+), 80 deletions(-) create mode 100644 client/src/features/settings/composables/useAuthorEnrichmentStatus.ts create mode 100644 server/src/db/migrations/0001_author_enrichment_queue.sql create mode 100644 server/src/db/migrations/0002_author_enrichment_single_processing_guard.sql create mode 100644 server/src/db/migrations/meta/0001_snapshot.json create mode 100644 server/src/db/migrations/meta/0002_snapshot.json create mode 100644 server/src/modules/authors/author-enrichment-executor.service.test.ts create mode 100644 server/src/modules/authors/author-enrichment-executor.service.ts create mode 100644 server/src/modules/authors/author-enrichment-orchestrator.service.test.ts create mode 100644 server/src/modules/authors/author-enrichment-orchestrator.service.ts create mode 100644 server/src/modules/authors/author-enrichment.gateway.test.ts create mode 100644 server/src/modules/authors/author-enrichment.gateway.ts create mode 100644 server/src/modules/authors/author-enrichment.repository.test.ts create mode 100644 server/src/modules/authors/author-enrichment.repository.ts create mode 100644 server/src/modules/authors/authors.repository.test.ts create mode 100644 server/src/modules/authors/metadata/author-metadata-fetch.service.test.ts create mode 100644 server/src/modules/metadata/metadata-events.service.ts diff --git a/client/src/features/settings/MaintenanceSettings.vue b/client/src/features/settings/MaintenanceSettings.vue index 8f17d5071..035f9f401 100644 --- a/client/src/features/settings/MaintenanceSettings.vue +++ b/client/src/features/settings/MaintenanceSettings.vue @@ -4,10 +4,11 @@ import { Check, RefreshCw, Sparkles, FileEdit } from 'lucide-vue-next' import { toast } from 'vue-sonner' import ToggleSwitch from '@/components/ui/ToggleSwitch.vue' import SettingsPageHeader from './SettingsPageHeader.vue' -import type { GlobalFileWriteSettings } from '@projectx/types' +import type { AuthorAutoEnrichmentWriteMode, GlobalFileWriteSettings } from '@projectx/types' import { DEFAULT_FILE_WRITE_SETTINGS } from '@projectx/types' import { api } from '@/lib/api' +import { useAuthorEnrichmentStatus } from './composables/useAuthorEnrichmentStatus' const running = ref(false) const queued = ref(null) @@ -33,6 +34,15 @@ async function rebuildEmbeddings() { const writeSettings = ref(structuredClone(DEFAULT_FILE_WRITE_SETTINGS)) const writeSaving = ref(false) +const authorAutoEnabled = ref(true) +const authorWriteMode = ref('missing_only') +const authorProviderEnabled = ref(true) +const authorSaving = ref(false) +const authorBackfillRunning = ref(false) +const { status: authorEnrichmentStatus, subscribe: subscribeAuthorEnrichmentStatus } = useAuthorEnrichmentStatus() +const isAuthorEnrichmentTaskRunning = computed( + () => authorEnrichmentStatus.value.processing > 0 || authorEnrichmentStatus.value.queued > 0 || authorEnrichmentStatus.value.rateLimited > 0, +) const epubMaxMb = computed({ get: () => Math.round(writeSettings.value.epub.maxFileSizeBytes / (1024 * 1024)), @@ -54,9 +64,17 @@ const cbxMaxMb = computed({ }) onMounted(async () => { - const res = await api('/api/v1/app-settings/file-write-settings') - if (res.ok) { - writeSettings.value = await res.json() + subscribeAuthorEnrichmentStatus() + const [writeRes, settingsRes] = await Promise.all([api('/api/v1/app-settings/file-write-settings'), api('/api/v1/app-settings')]) + if (writeRes.ok) { + writeSettings.value = await writeRes.json() + } + if (settingsRes.ok) { + const settings: { key: string; value: string }[] = await settingsRes.json() + const get = (key: string) => settings.find((s) => s.key === key)?.value + authorAutoEnabled.value = get('authors_auto_enrichment_enabled') !== 'false' + authorProviderEnabled.value = get('authors_provider_audnexus_enabled') !== 'false' + authorWriteMode.value = get('authors_auto_enrichment_write_mode') === 'always_refetch' ? 'always_refetch' : 'missing_only' } }) @@ -97,6 +115,76 @@ function toggleCbxFormat(fmt: 'cbz' | 'cb7') { } void saveWriteSettings() } + +async function saveAuthorSetting(key: string, value: string, successMessage: string): Promise { + if (authorSaving.value) return false + authorSaving.value = true + try { + const res = await api(`/api/v1/app-settings/${key}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ value }), + }) + if (res.ok) { + toast.success(successMessage) + return true + } else { + toast.error('Failed to save author enrichment setting') + } + } catch { + toast.error('Failed to save author enrichment setting') + } finally { + authorSaving.value = false + } + return false +} + +async function toggleAuthorAutoEnabled() { + const next = !authorAutoEnabled.value + const saved = await saveAuthorSetting( + 'authors_auto_enrichment_enabled', + String(next), + next ? 'Auto author enrichment enabled' : 'Auto author enrichment disabled', + ) + if (saved) { + authorAutoEnabled.value = next + } +} + +async function toggleAuthorProvider() { + const next = !authorProviderEnabled.value + const saved = await saveAuthorSetting( + 'authors_provider_audnexus_enabled', + String(next), + next ? 'Audnexus provider enabled' : 'Audnexus provider disabled', + ) + if (saved) { + authorProviderEnabled.value = next + } +} + +async function onAuthorWriteModeChange(event: Event) { + const mode = (event.target as HTMLSelectElement).value as AuthorAutoEnrichmentWriteMode + const saved = await saveAuthorSetting('authors_auto_enrichment_write_mode', mode, 'Author write mode updated') + if (saved) { + authorWriteMode.value = mode + } +} + +async function runAuthorBackfill() { + if (authorBackfillRunning.value) return + authorBackfillRunning.value = true + try { + const res = await api('/api/v1/authors/enrichment/backfill', { method: 'POST' }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data: { queued: number } = await res.json() + toast.success(`${data.queued} authors queued for enrichment`) + } catch { + toast.error('Failed to queue author backfill') + } finally { + authorBackfillRunning.value = false + } +}