"use server"; import { db } from "@/lib/db"; import { youtubeVideos, detectedObjects, marketplaceMatches, adminModeration } from "@/lib/db/schema"; import { eq, and, inArray, desc } from "drizzle-orm"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import { inngest } from "@/inngest/client"; import { revalidatePath } from "next/cache"; async function ensureAdmin() { const session = await auth.api.getSession({ headers: await headers() }); const adminEmails = process.env.ADMIN_EMAILS?.split(",") || []; if (!session?.user?.email || !adminEmails.includes(session.user.email)) { throw new Error("Unauthorized"); } } export async function getAnalysisQueue(status?: string) { await ensureAdmin(); const statuses = status ? [status] : ['pending_analysis', 'awaiting_approval', 'in_progress']; const queue = await db.query.youtubeVideos.findMany({ where: inArray(youtubeVideos.scanStatus, statuses as any), with: { channel: true }, orderBy: (youtubeVideos, { desc }) => [desc(youtubeVideos.createdAt)] }); return queue; } /** * Triggers AI analysis for a video that is currently in 'pending_analysis'. */ export async function triggerAIAnalysisAction(videoId: string) { await ensureAdmin(); const video = await db.query.youtubeVideos.findFirst({ where: eq(youtubeVideos.id, videoId) }); if (!video) throw new Error("Video not found"); // Send to Inngest await inngest.send({ name: 'youtube/video.detect-objects', data: { videoId: video.id, videoUrl: video.url || `https://www.youtube.com/watch?v=${video.videoId}`, }, }); // Note: status is updated to 'in_progress' inside the Inngest function. revalidatePath(`/admin/analysis/${videoId}`); return { success: true }; } /** * Approves a single detected object (making it visible to the user). */ export async function approveDetectionAction(detectionId: string) { const session = await auth.api.getSession({ headers: await headers() }); await ensureAdmin(); const detection = await db.query.detectedObjects.findFirst({ where: eq(detectedObjects.id, detectionId), with: { video: { with: { channel: true } } } }); if (!detection) throw new Error("Detection not found"); const isDemoRecord = detection.video?.channel?.creatorId?.startsWith('demo'); await db.transaction(async (tx) => { const updateData: any = { moderationStatus: 'APPROVED' }; // If it's a demo record, we auto-approve at the creator level too // so it appears in the public vault immediately. if (isDemoRecord) { updateData.status = 'approved'; } await tx.update(detectedObjects) .set(updateData) .where(eq(detectedObjects.id, detectionId)); await tx.insert(adminModeration).values({ detectionId, adminId: session!.user.id, action: 'approved', originalValues: { moderationStatus: detection.moderationStatus, status: detection.status }, correctedValues: updateData, trainAiFlag: true }); }); revalidatePath('/admin/analysis'); return { success: true }; } /** * Rejects a single detected object (hiding it from the user). */ export async function rejectDetectionAction(detectionId: string) { const session = await auth.api.getSession({ headers: await headers() }); await ensureAdmin(); const detection = await db.query.detectedObjects.findFirst({ where: eq(detectedObjects.id, detectionId) }); if (!detection) throw new Error("Detection not found"); await db.transaction(async (tx) => { await tx.update(detectedObjects) .set({ moderationStatus: 'REJECTED' }) .where(eq(detectedObjects.id, detectionId)); await tx.insert(adminModeration).values({ detectionId, adminId: session!.user.id, action: 'rejected', originalValues: { moderationStatus: detection.moderationStatus }, correctedValues: { moderationStatus: 'REJECTED' }, trainAiFlag: true }); }); revalidatePath('/admin/analysis'); return { success: true }; } /** * Marks the entire video analysis as complete and removes it from the queue. */ export async function completeVideoReviewAction(videoId: string) { await ensureAdmin(); await db.update(youtubeVideos) .set({ scanStatus: 'completed' }) .where(eq(youtubeVideos.id, videoId)); // Optional: Trigger notifications here // notifyUser(videoId, 'analysis_ready'); revalidatePath('/admin/analysis-queue'); revalidatePath(`/admin/analysis/${videoId}`); return { success: true }; } /** * Manually adds a product to a video from the admin interface. */ export async function addManualProductAction(videoId: string, data: { objectName: string; category: any; price?: number; affiliateUrl?: string; imageUrl?: string; }) { await ensureAdmin(); // Check if it's a demo record const videoResult = await db.query.youtubeVideos.findFirst({ where: eq(youtubeVideos.id, videoId), with: { channel: true } }); const isDemoRecord = videoResult?.channel?.creatorId?.startsWith('demo'); // 1. Create Detection (AUTO-APPROVED if added by admin) const [detection] = await db.insert(detectedObjects).values({ videoId, objectName: data.objectName, category: data.category, confidenceScore: 1.0, frameTimestamp: 0, status: isDemoRecord ? 'approved' : 'pending_review', moderationStatus: 'APPROVED', thumbnailUrl: data.imageUrl || '/placeholder-product.png', }).returning(); // 2. Create Marketplace Match if URL provided if (data.affiliateUrl) { // Simple productId extraction fallback const productId = data.affiliateUrl.match(/\/dp\/([A-Z0-9]{10})/) ? data.affiliateUrl.match(/\/dp\/([A-Z0-9]{10})/)?.[1] : `manual-${crypto.randomUUID().slice(0, 8)}`; await db.insert(marketplaceMatches).values({ objectId: detection.id, marketplace: 'amazon', productId: productId || 'unknown', productName: data.objectName, price: data.price || 0, availabilityStatus: 'IN_STOCK', affiliateUrl: data.affiliateUrl, imageUrl: data.imageUrl, }); } revalidatePath(`/admin/analysis/${videoId}`); return { success: true }; } /** * Fetches products from other vaults for the same videoId. */ export async function getCrossVaultSuggestions(videoId: string, excludeCreatorId?: string) { await ensureAdmin(); // Find other instances of this videoId const otherVideos = await db.query.youtubeVideos.findMany({ where: eq(youtubeVideos.videoId, videoId), }); const otherVideoIds = otherVideos.map(v => v.id); if (otherVideoIds.length === 0) return []; // Fetch unique products from these videos const products = await db.query.detectedObjects.findMany({ where: and( inArray(detectedObjects.videoId, otherVideoIds), eq(detectedObjects.moderationStatus, 'APPROVED') ), with: { marketplaceMatches: true } }); // Deduplicate by product name/link return products; } /** * Clones a detection (and its first marketplace match) to the specified video. */ export async function cloneDetectionAction(videoId: string, sourceDetectionId: string) { await ensureAdmin(); const source = await db.query.detectedObjects.findFirst({ where: eq(detectedObjects.id, sourceDetectionId), with: { marketplaceMatches: true } }); if (!source) throw new Error("Source detection not found"); // 1. Create new detection const [newDetection] = await db.insert(detectedObjects).values({ videoId, objectName: source.objectName, category: source.category, confidenceScore: 1.0, frameTimestamp: source.frameTimestamp, thumbnailUrl: source.thumbnailUrl, status: 'pending_review', moderationStatus: 'APPROVED', }).returning(); // 2. Clone first marketplace match const match = source.marketplaceMatches[0]; if (match) { await db.insert(marketplaceMatches).values({ objectId: newDetection.id, marketplace: match.marketplace, productId: match.productId, productName: match.productName, price: match.price, availabilityStatus: match.availabilityStatus, affiliateUrl: match.affiliateUrl, imageUrl: match.imageUrl, }); } revalidatePath(`/admin/analysis/${videoId}`); return { success: true }; } /** * Fetches the moderation history for a specific video. */ export async function getAnalysisHistory(videoInternalId: string) { await ensureAdmin(); // Get all detections for this video const detections = await db.query.detectedObjects.findMany({ where: eq(detectedObjects.videoId, videoInternalId), }); if (detections.length === 0) return []; // Get audit logs for these detections const logs = await db.query.adminModeration.findMany({ where: inArray(adminModeration.detectionId, detections.map(d => d.id)), with: { admin: true, detection: true, }, orderBy: [desc(adminModeration.createdAt)], }); return logs; }