Add reported-issues, Goodreads sync & notifs

Introduce user-reported-issues and Goodreads shelf sync features and wire them into notifications. Adds Prisma migrations and schema changes (ReportedIssue, GoodreadsShelf, GoodreadsBookMapping), API endpoints for reporting (POST /audiobooks/[asin]/report-issue) and admin management (list, resolve/dismiss, replace), and an admin UI section to view/dismiss/replace reported issues. Adds a new notification event (issue_reported) with updates to notification schemas, docs and provider handling, plus a notification-events constants file. Refactors request creation to use createRequestForUser service, adds a Goodreads sync processor/service/hooks/UI modals, a scrape-resilience util, and related tests and minor integration updates.
This commit is contained in:
kikootwo
2026-02-11 16:49:55 -05:00
parent b013538b63
commit 20c8fb0898
69 changed files with 4167 additions and 766 deletions
+41 -4
View File
@@ -9,6 +9,7 @@ import { prisma } from '../db';
import { TorrentResult } from '../utils/ranking-algorithm';
import { DownloadClientType } from '../interfaces/download-client.interface';
import { RMABLogger } from '../utils/logger';
import type { NotificationEvent } from '@/lib/constants/notification-events';
const logger = RMABLogger.create('JobQueue');
@@ -25,6 +26,7 @@ export type JobType =
| 'retry_failed_imports'
| 'cleanup_seeded_torrents'
| 'monitor_rss_feeds'
| 'sync_goodreads_shelves'
| 'send_notification'
// Ebook-specific job types
| 'search_ebook'
@@ -100,6 +102,12 @@ export interface CleanupSeededTorrentsPayload extends JobPayload {
scheduledJobId?: string;
}
export interface SyncGoodreadsShelvesPayload extends JobPayload {
scheduledJobId?: string;
shelfId?: string;
maxLookupsPerShelf?: number;
}
// Ebook-specific payload interfaces
export interface SearchEbookPayload extends JobPayload {
requestId: string;
@@ -140,8 +148,9 @@ export interface MonitorDirectDownloadPayload extends JobPayload {
}
export interface SendNotificationPayload extends JobPayload {
event: 'request_pending_approval' | 'request_approved' | 'request_available' | 'request_error';
requestId: string;
event: NotificationEvent;
requestId?: string;
issueId?: string;
title: string;
author: string;
userName: string;
@@ -340,6 +349,12 @@ export class JobQueueService {
return await processCleanupSeededTorrents(payloadWithJobId);
});
this.queue.process('sync_goodreads_shelves', 1, async (job: BullJob<SyncGoodreadsShelvesPayload>) => {
const { processSyncGoodreadsShelves } = await import('../processors/sync-goodreads-shelves.processor');
const payloadWithJobId = await this.ensureJobRecord(job, 'sync_goodreads_shelves');
return await processSyncGoodreadsShelves(payloadWithJobId);
});
// Send notification processor
this.queue.process('send_notification', 5, async (job: BullJob<SendNotificationPayload>) => {
const { processSendNotification } = await import('../processors/send-notification.processor');
@@ -695,6 +710,23 @@ export class JobQueueService {
);
}
/**
* Add sync Goodreads shelves job
*/
async addSyncGoodreadsShelvesJob(scheduledJobId?: string, shelfId?: string, maxLookupsPerShelf?: number): Promise<string> {
return await this.addJob(
'sync_goodreads_shelves',
{
scheduledJobId,
shelfId,
maxLookupsPerShelf,
} as SyncGoodreadsShelvesPayload,
{
priority: 7,
}
);
}
// =========================================================================
// EBOOK-SPECIFIC JOB METHODS
// =========================================================================
@@ -911,7 +943,7 @@ export class JobQueueService {
* Add notification job
*/
async addNotificationJob(
event: 'request_pending_approval' | 'request_approved' | 'request_available' | 'request_error',
event: NotificationEvent,
requestId: string,
title: string,
author: string,
@@ -923,11 +955,16 @@ export class JobQueueService {
'send_notification',
{
event,
requestId,
// issue_reported passes an issue ID, not a request ID — omit from payload
// so addJob doesn't try to create a FK to the requests table.
// The ID is still available in the notification payload for display.
requestId: event === 'issue_reported' ? undefined : requestId,
title,
author,
userName,
message,
// Pass the original ID for notification display (e.g., Discord footer)
...(event === 'issue_reported' && { issueId: requestId }),
timestamp: new Date(),
} as SendNotificationPayload,
{