From 89882a545b60cafe6c011d042c1966461b36354d Mon Sep 17 00:00:00 2001 From: Martin Porwoll Date: Fri, 16 Jan 2026 21:33:42 +0000 Subject: [PATCH] feat(community): Phase 2.4 - Unified Sync Service Implements a unified sync service that orchestrates comment synchronization across all social media platforms. UnifiedSyncService: - Platform-agnostic sync orchestration - Support for YouTube, Facebook, and Instagram - Parallel platform detection and account grouping - Progress tracking with live status updates - Aggregated results per platform - Error handling with partial results support New API Endpoints: - GET/POST /api/cron/community-sync - Cron endpoint for scheduled multi-platform sync - Query params: platforms, accountIds, analyzeWithAI, maxItems - HEAD for monitoring status - GET /api/community/sync-status - Live sync status for dashboard - Platform overview with account details - Interaction statistics (total, today, unanswered) - Last sync result summary Configuration: - vercel.json updated to use community-sync cron - 15-minute sync interval maintained Co-Authored-By: Claude Opus 4.5 --- .../api/community/sync-status/route.ts | 158 +++++ .../api/cron/community-sync/route.ts | 203 +++++++ src/lib/jobs/UnifiedSyncService.ts | 554 ++++++++++++++++++ vercel.json | 2 +- 4 files changed, 916 insertions(+), 1 deletion(-) create mode 100644 src/app/(payload)/api/community/sync-status/route.ts create mode 100644 src/app/(payload)/api/cron/community-sync/route.ts create mode 100644 src/lib/jobs/UnifiedSyncService.ts diff --git a/src/app/(payload)/api/community/sync-status/route.ts b/src/app/(payload)/api/community/sync-status/route.ts new file mode 100644 index 0000000..dde891e --- /dev/null +++ b/src/app/(payload)/api/community/sync-status/route.ts @@ -0,0 +1,158 @@ +// src/app/(payload)/api/community/sync-status/route.ts +// Sync Status API für Dashboard + +import { NextRequest, NextResponse } from 'next/server' +import { getPayload } from 'payload' +import config from '@payload-config' +import { getUnifiedSyncStatus } from '@/lib/jobs/UnifiedSyncService' +import { getSyncStatus as getYouTubeSyncStatus } from '@/lib/jobs/syncAllComments' + +/** + * GET /api/community/sync-status + * Gibt den aktuellen Sync-Status für alle Plattformen zurück + */ +export async function GET(request: NextRequest) { + try { + const payload = await getPayload({ config }) + + // Authentifizierung prüfen + const { user } = await payload.auth({ headers: request.headers }) + + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Sync-Status abrufen + const unifiedStatus = getUnifiedSyncStatus() + const youtubeStatus = getYouTubeSyncStatus() + + // Account-Statistiken laden + const accounts = await payload.find({ + collection: 'social-accounts', + where: { + isActive: { equals: true }, + }, + limit: 100, + depth: 2, + }) + + // Nach Plattform aggregieren + const platformStats: Record< + string, + { + platform: string + accountCount: number + accounts: Array<{ + id: number + name: string + lastSyncedAt: string | null + followers: number + totalPosts: number + }> + } + > = {} + + for (const account of accounts.docs) { + const platformDoc = account.platform as { slug?: string; name?: string } | undefined + const platformSlug = platformDoc?.slug || 'unknown' + const platformName = platformDoc?.name || platformSlug + + if (!platformStats[platformSlug]) { + platformStats[platformSlug] = { + platform: platformName, + accountCount: 0, + accounts: [], + } + } + + const stats = account.stats as { + lastSyncedAt?: string + followers?: number + totalPosts?: number + } | undefined + + platformStats[platformSlug].accountCount++ + platformStats[platformSlug].accounts.push({ + id: account.id as number, + name: (account.displayName as string) || `Account ${account.id}`, + lastSyncedAt: stats?.lastSyncedAt || null, + followers: stats?.followers || 0, + totalPosts: stats?.totalPosts || 0, + }) + } + + // Interaktions-Statistiken + const interactionStats = await payload.find({ + collection: 'community-interactions', + limit: 0, // Nur Count + }) + + // Neue Interaktionen heute + const todayStart = new Date() + todayStart.setHours(0, 0, 0, 0) + + const todayInteractions = await payload.find({ + collection: 'community-interactions', + where: { + createdAt: { greater_than: todayStart.toISOString() }, + }, + limit: 0, + }) + + // Unbeantwortete Interaktionen + const unansweredInteractions = await payload.find({ + collection: 'community-interactions', + where: { + status: { in: ['new', 'in_review'] }, + }, + limit: 0, + }) + + return NextResponse.json({ + // Live Sync-Status + syncStatus: { + isRunning: unifiedStatus.isRunning || youtubeStatus.isRunning, + currentPlatform: unifiedStatus.currentPlatform, + currentAccount: unifiedStatus.currentAccount, + progress: unifiedStatus.progress, + lastRunAt: unifiedStatus.lastRunAt || youtubeStatus.lastRunAt, + }, + + // Plattform-Übersicht + platforms: Object.values(platformStats), + + // Interaktions-Statistiken + interactions: { + total: interactionStats.totalDocs, + today: todayInteractions.totalDocs, + unanswered: unansweredInteractions.totalDocs, + }, + + // Letztes Sync-Ergebnis + lastSyncResult: unifiedStatus.lastResult + ? { + success: unifiedStatus.lastResult.success, + startedAt: unifiedStatus.lastResult.startedAt, + completedAt: unifiedStatus.lastResult.completedAt, + duration: unifiedStatus.lastResult.duration, + platforms: unifiedStatus.lastResult.platforms, + totalNewComments: unifiedStatus.lastResult.results.reduce( + (sum, r) => sum + r.newComments, + 0 + ), + totalUpdatedComments: unifiedStatus.lastResult.results.reduce( + (sum, r) => sum + r.updatedComments, + 0 + ), + totalErrors: unifiedStatus.lastResult.errors.length, + } + : null, + }) + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + console.error('[API] sync-status error:', error) + return NextResponse.json({ error: message }, { status: 500 }) + } +} + +export const dynamic = 'force-dynamic' diff --git a/src/app/(payload)/api/cron/community-sync/route.ts b/src/app/(payload)/api/cron/community-sync/route.ts new file mode 100644 index 0000000..8031148 --- /dev/null +++ b/src/app/(payload)/api/cron/community-sync/route.ts @@ -0,0 +1,203 @@ +// src/app/(payload)/api/cron/community-sync/route.ts +// Unified Community Sync Cron Endpoint + +import { NextRequest, NextResponse } from 'next/server' +import { + runUnifiedSync, + getUnifiedSyncStatus, + type SupportedPlatform, + type UnifiedSyncOptions, +} from '@/lib/jobs/UnifiedSyncService' + +// Geheimer Token für Cron-Authentifizierung +const CRON_SECRET = process.env.CRON_SECRET + +/** + * GET /api/cron/community-sync + * Wird von externem Cron-Job aufgerufen (z.B. Vercel Cron, cron-job.org) + * + * Query Parameters: + * - platforms: Komma-separierte Liste (youtube,facebook,instagram) + * - accountIds: Komma-separierte Account-IDs + * - analyzeWithAI: true/false (default: true) + * - maxItems: Maximale Items pro Account (default: 100) + */ +export async function GET(request: NextRequest) { + // Auth prüfen wenn CRON_SECRET gesetzt + if (CRON_SECRET) { + const authHeader = request.headers.get('authorization') + + if (authHeader !== `Bearer ${CRON_SECRET}`) { + console.warn('[Cron] Unauthorized request to community-sync') + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + } + + // Query-Parameter parsen + const searchParams = request.nextUrl.searchParams + const options: UnifiedSyncOptions = {} + + // Plattform-Filter + const platformsParam = searchParams.get('platforms') + if (platformsParam) { + const platforms = platformsParam.split(',').map((p) => p.trim().toLowerCase()) + const validPlatforms = platforms.filter((p): p is SupportedPlatform => + ['youtube', 'facebook', 'instagram'].includes(p) + ) + if (validPlatforms.length > 0) { + options.platforms = validPlatforms + } + } + + // Account-ID Filter + const accountIdsParam = searchParams.get('accountIds') + if (accountIdsParam) { + const ids = accountIdsParam + .split(',') + .map((id) => parseInt(id.trim(), 10)) + .filter((id) => !isNaN(id)) + if (ids.length > 0) { + options.accountIds = ids + } + } + + // AI-Analyse + const analyzeParam = searchParams.get('analyzeWithAI') + if (analyzeParam !== null) { + options.analyzeWithAI = analyzeParam !== 'false' + } + + // Max Items + const maxItemsParam = searchParams.get('maxItems') + if (maxItemsParam) { + const maxItems = parseInt(maxItemsParam, 10) + if (!isNaN(maxItems) && maxItems > 0) { + options.maxItemsPerAccount = maxItems + } + } + + console.log('[Cron] Starting community sync', { options }) + + const result = await runUnifiedSync(options) + + if (result.success) { + // Gesamtstatistiken berechnen + let totalNew = 0 + let totalUpdated = 0 + let totalErrors = 0 + + for (const platform of Object.values(result.platforms)) { + if (platform) { + totalNew += platform.totalNewComments + totalUpdated += platform.totalUpdatedComments + totalErrors += platform.totalErrors + } + } + + return NextResponse.json({ + success: true, + message: `Sync completed: ${totalNew} new, ${totalUpdated} updated comments across ${result.results.length} accounts`, + duration: result.duration, + platforms: result.platforms, + results: result.results, + }) + } + + return NextResponse.json( + { + success: false, + errors: result.errors, + partialResults: result.results, + }, + { status: 500 } + ) +} + +/** + * POST /api/cron/community-sync + * Manueller Sync-Trigger mit erweiterten Optionen + */ +export async function POST(request: NextRequest) { + // Auth prüfen + if (CRON_SECRET) { + const authHeader = request.headers.get('authorization') + + if (authHeader !== `Bearer ${CRON_SECRET}`) { + console.warn('[Cron] Unauthorized POST to community-sync') + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + } + + try { + const body = await request.json() + const options: UnifiedSyncOptions = {} + + // Optionen aus Body übernehmen + if (body.platforms && Array.isArray(body.platforms)) { + options.platforms = body.platforms.filter((p: string): p is SupportedPlatform => + ['youtube', 'facebook', 'instagram'].includes(p) + ) + } + + if (body.accountIds && Array.isArray(body.accountIds)) { + options.accountIds = body.accountIds.filter( + (id: unknown) => typeof id === 'number' && id > 0 + ) + } + + if (typeof body.analyzeWithAI === 'boolean') { + options.analyzeWithAI = body.analyzeWithAI + } + + if (typeof body.maxItemsPerAccount === 'number' && body.maxItemsPerAccount > 0) { + options.maxItemsPerAccount = body.maxItemsPerAccount + } + + if (body.sinceDate) { + const date = new Date(body.sinceDate) + if (!isNaN(date.getTime())) { + options.sinceDate = date + } + } + + console.log('[Cron] Manual community sync triggered', { options }) + + const result = await runUnifiedSync(options) + + return NextResponse.json({ + success: result.success, + startedAt: result.startedAt, + completedAt: result.completedAt, + duration: result.duration, + platforms: result.platforms, + results: result.results, + errors: result.errors, + }) + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + console.error('[Cron] POST error:', error) + return NextResponse.json({ error: message }, { status: 500 }) + } +} + +/** + * HEAD /api/cron/community-sync + * Status-Check für Monitoring + */ +export async function HEAD() { + const status = getUnifiedSyncStatus() + + return new NextResponse(null, { + status: status.isRunning ? 423 : 200, // 423 = Locked (running) + headers: { + 'X-Sync-Running': status.isRunning.toString(), + 'X-Current-Platform': status.currentPlatform || 'none', + 'X-Current-Account': status.currentAccount || 'none', + 'X-Progress': `${status.progress.percentage}%`, + 'X-Last-Run': status.lastRunAt || 'never', + }, + }) +} + +export const dynamic = 'force-dynamic' +export const maxDuration = 300 // 5 Minuten max (Vercel) diff --git a/src/lib/jobs/UnifiedSyncService.ts b/src/lib/jobs/UnifiedSyncService.ts new file mode 100644 index 0000000..8cc9a18 --- /dev/null +++ b/src/lib/jobs/UnifiedSyncService.ts @@ -0,0 +1,554 @@ +// src/lib/jobs/UnifiedSyncService.ts +// Unified Sync Service für alle Social Media Plattformen + +import { getPayload, Payload } from 'payload' +import config from '@payload-config' +import { CommentsSyncService } from '../integrations/youtube/CommentsSyncService' +import { FacebookSyncService } from '../integrations/meta/FacebookSyncService' +import { InstagramSyncService } from '../integrations/meta/InstagramSyncService' +import { JobLogger } from './JobLogger' + +// ============================================================================= +// Types +// ============================================================================= + +export type SupportedPlatform = 'youtube' | 'facebook' | 'instagram' + +export interface PlatformSyncResult { + platform: SupportedPlatform + accountId: number + accountName: string + success: boolean + newComments: number + updatedComments: number + newMessages: number + postsProcessed: number + errors: string[] + duration: number +} + +export interface UnifiedSyncResult { + success: boolean + startedAt: Date + completedAt: Date + duration: number + platforms: { + [key in SupportedPlatform]?: { + accountsProcessed: number + totalNewComments: number + totalUpdatedComments: number + totalErrors: number + } + } + results: PlatformSyncResult[] + errors: string[] +} + +export interface UnifiedSyncOptions { + /** Nur bestimmte Plattformen synchronisieren */ + platforms?: SupportedPlatform[] + /** Nur bestimmte Account-IDs synchronisieren */ + accountIds?: number[] + /** AI-Analyse aktivieren */ + analyzeWithAI?: boolean + /** Maximale Items pro Account */ + maxItemsPerAccount?: number + /** Nur seit diesem Datum synchronisieren */ + sinceDate?: Date +} + +export interface SyncStatus { + isRunning: boolean + currentPlatform: SupportedPlatform | null + currentAccount: string | null + progress: { + totalAccounts: number + processedAccounts: number + percentage: number + } + lastRunAt: string | null + lastResult: UnifiedSyncResult | null +} + +// ============================================================================= +// Global State +// ============================================================================= + +let syncStatus: SyncStatus = { + isRunning: false, + currentPlatform: null, + currentAccount: null, + progress: { + totalAccounts: 0, + processedAccounts: 0, + percentage: 0, + }, + lastRunAt: null, + lastResult: null, +} + +// ============================================================================= +// Platform Detection +// ============================================================================= + +function detectPlatform(platformDoc: any): SupportedPlatform | null { + const slug = platformDoc?.slug?.toLowerCase() + const apiType = platformDoc?.apiConfig?.apiType + + if (slug === 'youtube' || apiType === 'youtube_v3') { + return 'youtube' + } + if (slug === 'facebook' || apiType === 'facebook_graph' || apiType === 'meta_graph') { + return 'facebook' + } + if (slug === 'instagram' || apiType === 'instagram_graph') { + return 'instagram' + } + + return null +} + +// ============================================================================= +// Unified Sync Service +// ============================================================================= + +export class UnifiedSyncService { + private payload: Payload + private logger: JobLogger + private youtubeSyncService: CommentsSyncService + private facebookSyncService: FacebookSyncService + private instagramSyncService: InstagramSyncService + + constructor(payload: Payload) { + this.payload = payload + this.logger = new JobLogger('unified-sync') + this.youtubeSyncService = new CommentsSyncService(payload) + this.facebookSyncService = new FacebookSyncService(payload) + this.instagramSyncService = new InstagramSyncService(payload) + } + + /** + * Führt einen vollständigen Sync aller Plattformen durch + */ + async runFullSync(options: UnifiedSyncOptions = {}): Promise { + const { + platforms, + accountIds, + analyzeWithAI = true, + maxItemsPerAccount = 100, + sinceDate, + } = options + + const startedAt = new Date() + const results: PlatformSyncResult[] = [] + const errors: string[] = [] + + // Prüfen ob bereits ein Sync läuft + if (syncStatus.isRunning) { + return { + success: false, + startedAt, + completedAt: new Date(), + duration: 0, + platforms: {}, + results: [], + errors: ['Sync already running'], + } + } + + syncStatus.isRunning = true + syncStatus.progress = { totalAccounts: 0, processedAccounts: 0, percentage: 0 } + + try { + this.logger.info('Starting unified sync', { platforms, accountIds }) + + // Alle aktiven Accounts laden + const accounts = await this.loadActiveAccounts(platforms, accountIds) + syncStatus.progress.totalAccounts = accounts.length + + this.logger.info(`Found ${accounts.length} active accounts to sync`) + + if (accounts.length === 0) { + return this.createResult(startedAt, results, errors) + } + + // Accounts nach Plattform gruppieren + const accountsByPlatform = this.groupAccountsByPlatform(accounts) + + // Jede Plattform synchronisieren + for (const [platform, platformAccounts] of Object.entries(accountsByPlatform)) { + syncStatus.currentPlatform = platform as SupportedPlatform + + for (const account of platformAccounts) { + syncStatus.currentAccount = account.displayName || `Account ${account.id}` + + try { + const result = await this.syncAccount( + platform as SupportedPlatform, + account, + { + analyzeWithAI, + maxItems: maxItemsPerAccount, + sinceDate, + } + ) + results.push(result) + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + errors.push(`${platform}/${account.id}: ${errorMsg}`) + this.logger.error(`Failed to sync account ${account.id}`, error) + } + + syncStatus.progress.processedAccounts++ + syncStatus.progress.percentage = Math.round( + (syncStatus.progress.processedAccounts / syncStatus.progress.totalAccounts) * 100 + ) + + // Rate Limiting zwischen Accounts + await this.sleep(500) + } + } + + const finalResult = this.createResult(startedAt, results, errors) + syncStatus.lastResult = finalResult + syncStatus.lastRunAt = startedAt.toISOString() + + this.logger.summary({ + success: finalResult.success, + processed: results.length, + errors: errors.length, + }) + + return finalResult + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + this.logger.error('Unified sync failed', error) + errors.push(errorMsg) + return this.createResult(startedAt, results, errors) + } finally { + syncStatus.isRunning = false + syncStatus.currentPlatform = null + syncStatus.currentAccount = null + } + } + + /** + * Lädt alle aktiven Social Accounts + */ + private async loadActiveAccounts( + platforms?: SupportedPlatform[], + accountIds?: number[] + ): Promise { + const where: any = { + and: [{ isActive: { equals: true } }], + } + + // Account-ID Filter + if (accountIds && accountIds.length > 0) { + where.and.push({ + id: { in: accountIds }, + }) + } + + const accounts = await this.payload.find({ + collection: 'social-accounts', + where, + limit: 100, + depth: 2, + }) + + // Plattform-Filter anwenden + if (platforms && platforms.length > 0) { + return accounts.docs.filter((account) => { + const platform = detectPlatform(account.platform) + return platform && platforms.includes(platform) + }) + } + + // Nur Accounts mit unterstützter Plattform zurückgeben + return accounts.docs.filter((account) => { + const platform = detectPlatform(account.platform) + return platform !== null + }) + } + + /** + * Gruppiert Accounts nach Plattform + */ + private groupAccountsByPlatform(accounts: any[]): Record { + const grouped: Record = { + youtube: [], + facebook: [], + instagram: [], + } + + for (const account of accounts) { + const platform = detectPlatform(account.platform) + if (platform) { + grouped[platform].push(account) + } + } + + return grouped + } + + /** + * Synchronisiert einen einzelnen Account + */ + private async syncAccount( + platform: SupportedPlatform, + account: any, + options: { + analyzeWithAI: boolean + maxItems: number + sinceDate?: Date + } + ): Promise { + const { analyzeWithAI, maxItems, sinceDate } = options + const accountId = account.id as number + const accountName = (account.displayName as string) || `Account ${accountId}` + const startTime = Date.now() + + this.logger.info(`Syncing ${platform} account: ${accountName}`) + + // Default since date: letzte 7 Tage oder letzter Sync + const stats = account.stats as { lastSyncedAt?: string } | undefined + const effectiveSinceDate = + sinceDate || + (stats?.lastSyncedAt + ? new Date(stats.lastSyncedAt) + : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)) + + try { + let result: PlatformSyncResult + + switch (platform) { + case 'youtube': + result = await this.syncYouTubeAccount(accountId, accountName, { + sinceDate: effectiveSinceDate, + maxItems, + analyzeWithAI, + }) + break + + case 'facebook': + result = await this.syncFacebookAccount(accountId, accountName, { + sinceDate: effectiveSinceDate, + maxItems, + analyzeWithAI, + }) + break + + case 'instagram': + result = await this.syncInstagramAccount(accountId, accountName, { + sinceDate: effectiveSinceDate, + maxItems, + analyzeWithAI, + }) + break + + default: + throw new Error(`Unsupported platform: ${platform}`) + } + + result.duration = Date.now() - startTime + + this.logger.info( + `${platform}/${accountName}: ${result.newComments} new, ${result.updatedComments} updated` + ) + + return result + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + return { + platform, + accountId, + accountName, + success: false, + newComments: 0, + updatedComments: 0, + newMessages: 0, + postsProcessed: 0, + errors: [errorMsg], + duration: Date.now() - startTime, + } + } + } + + /** + * YouTube Account synchronisieren + */ + private async syncYouTubeAccount( + accountId: number, + accountName: string, + options: { sinceDate: Date; maxItems: number; analyzeWithAI: boolean } + ): Promise { + const syncResult = await this.youtubeSyncService.syncComments({ + socialAccountId: accountId, + sinceDate: options.sinceDate, + maxComments: options.maxItems, + analyzeWithAI: options.analyzeWithAI, + }) + + return { + platform: 'youtube', + accountId, + accountName, + success: syncResult.success, + newComments: syncResult.newComments, + updatedComments: syncResult.updatedComments, + newMessages: 0, + postsProcessed: 0, + errors: syncResult.errors, + duration: 0, + } + } + + /** + * Facebook Account synchronisieren + */ + private async syncFacebookAccount( + accountId: number, + accountName: string, + options: { sinceDate: Date; maxItems: number; analyzeWithAI: boolean } + ): Promise { + const syncResult = await this.facebookSyncService.syncComments({ + socialAccountId: accountId, + sinceDate: options.sinceDate, + maxItems: options.maxItems, + analyzeWithAI: options.analyzeWithAI, + syncComments: true, + syncMessages: false, + }) + + return { + platform: 'facebook', + accountId, + accountName, + success: syncResult.errors.length === 0, + newComments: syncResult.newComments, + updatedComments: syncResult.updatedComments, + newMessages: syncResult.newMessages, + postsProcessed: syncResult.postsProcessed, + errors: syncResult.errors, + duration: syncResult.duration, + } + } + + /** + * Instagram Account synchronisieren + */ + private async syncInstagramAccount( + accountId: number, + accountName: string, + options: { sinceDate: Date; maxItems: number; analyzeWithAI: boolean } + ): Promise { + const syncResult = await this.instagramSyncService.syncComments({ + socialAccountId: accountId, + sinceDate: options.sinceDate, + maxItems: options.maxItems, + analyzeWithAI: options.analyzeWithAI, + syncComments: true, + syncMentions: true, + }) + + return { + platform: 'instagram', + accountId, + accountName, + success: syncResult.errors.length === 0, + newComments: syncResult.newComments, + updatedComments: syncResult.updatedComments, + newMessages: syncResult.newMessages, + postsProcessed: syncResult.postsProcessed, + errors: syncResult.errors, + duration: syncResult.duration, + } + } + + /** + * Erstellt das finale Sync-Ergebnis + */ + private createResult( + startedAt: Date, + results: PlatformSyncResult[], + errors: string[] + ): UnifiedSyncResult { + const completedAt = new Date() + + // Ergebnisse nach Plattform aggregieren + const platforms: UnifiedSyncResult['platforms'] = {} + + for (const result of results) { + if (!platforms[result.platform]) { + platforms[result.platform] = { + accountsProcessed: 0, + totalNewComments: 0, + totalUpdatedComments: 0, + totalErrors: 0, + } + } + + platforms[result.platform]!.accountsProcessed++ + platforms[result.platform]!.totalNewComments += result.newComments + platforms[result.platform]!.totalUpdatedComments += result.updatedComments + platforms[result.platform]!.totalErrors += result.errors.length + } + + return { + success: errors.length === 0 && results.every((r) => r.success), + startedAt, + completedAt, + duration: completedAt.getTime() - startedAt.getTime(), + platforms, + results, + errors, + } + } + + private sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) + } +} + +// ============================================================================= +// Convenience Functions +// ============================================================================= + +/** + * Führt einen vollständigen Sync aus (Singleton-Pattern) + */ +export async function runUnifiedSync( + options: UnifiedSyncOptions = {} +): Promise { + const payload = await getPayload({ config }) + const service = new UnifiedSyncService(payload) + return service.runFullSync(options) +} + +/** + * Gibt den aktuellen Sync-Status zurück + */ +export function getUnifiedSyncStatus(): SyncStatus { + return { ...syncStatus } +} + +/** + * Setzt den Sync-Status zurück (für Tests) + */ +export function resetUnifiedSyncStatus(): void { + syncStatus = { + isRunning: false, + currentPlatform: null, + currentAccount: null, + progress: { + totalAccounts: 0, + processedAccounts: 0, + percentage: 0, + }, + lastRunAt: null, + lastResult: null, + } +} + +export default UnifiedSyncService diff --git a/vercel.json b/vercel.json index bac81af..d410113 100644 --- a/vercel.json +++ b/vercel.json @@ -2,7 +2,7 @@ "$schema": "https://openapi.vercel.sh/vercel.json", "crons": [ { - "path": "/api/cron/youtube-sync", + "path": "/api/cron/community-sync", "schedule": "*/15 * * * *" } ]