diff --git a/client/src/components/ongoing-reels.tsx b/client/src/components/ongoing-reels.tsx new file mode 100644 index 0000000..a538174 --- /dev/null +++ b/client/src/components/ongoing-reels.tsx @@ -0,0 +1,109 @@ +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Clapperboard, Loader2, CheckCircle2, XCircle } from "lucide-react"; +import type { Post } from "@shared/schema"; + +/** Labels for each progress stage */ +function getStageLabel(progress: number): string { + if (progress <= 5) return "Démarrage du traitement…"; + if (progress <= 15) return "Traitement vidéo (FFmpeg)…"; + if (progress <= 65) return "Encodage en cours…"; + if (progress <= 85) return "Upload vers le cloud…"; + if (progress <= 90) return "Liaison du média…"; + if (progress <= 95) return "Publication en cours…"; + return "Finalisation…"; +} + +interface OngoingReelsProps { + /** Compact layout for mobile */ + compact?: boolean; +} + +export default function OngoingReels({ compact = false }: OngoingReelsProps) { + const { data: ongoingPosts = [] } = useQuery({ + queryKey: ["/api/reels/ongoing"], + refetchInterval: 3000, // Poll every 3 seconds + }); + + // Nothing to show — render nothing (not even a card) + if (ongoingPosts.length === 0) { + return null; + } + + return ( + + + + + Génération en cours + + + +
+ {ongoingPosts.map((post) => { + const progress = post.generationProgress ?? 0; + const isFailed = post.generationStatus === "failed"; + + return ( +
+ {/* Icon */} +
+ +
+ + {/* Content */} +
+

+ {post.content || "Reel sans texte"} +

+ + {isFailed ? ( +
+ + + {post.generationError || "Erreur lors du traitement"} + +
+ ) : ( + <> +
+ +
+
+ + {getStageLabel(progress)} + + + {progress}% + +
+ + )} + + {progress >= 100 && !isFailed && ( +
+ + Terminé +
+ )} +
+
+ ); + })} +
+
+
+ ); +} diff --git a/client/src/pages/dashboard.tsx b/client/src/pages/dashboard.tsx index d097e43..a4de55c 100644 --- a/client/src/pages/dashboard.tsx +++ b/client/src/pages/dashboard.tsx @@ -1,6 +1,7 @@ import Sidebar from "@/components/sidebar"; import TopBar from "@/components/topbar"; import StatsCards from "@/components/stats-cards"; +import OngoingReels from "@/components/ongoing-reels"; import RecentPublications from "@/components/recent-publications"; import ManagedPages from "@/components/managed-pages"; import { useState } from "react"; @@ -11,12 +12,12 @@ export default function Dashboard() { return (
{sidebarOpen && ( -
setSidebarOpen(false)} /> )} - +
setSidebarOpen(!sidebarOpen)} /> - +
- + + + - +
diff --git a/client/src/pages/mobile/dashboard.tsx b/client/src/pages/mobile/dashboard.tsx index ddc7edc..e866e94 100644 --- a/client/src/pages/mobile/dashboard.tsx +++ b/client/src/pages/mobile/dashboard.tsx @@ -1,6 +1,7 @@ import Sidebar from "@/components/sidebar"; import TopBar from "@/components/topbar"; import StatsCards from "@/components/stats-cards"; +import OngoingReels from "@/components/ongoing-reels"; import RecentPublications from "@/components/recent-publications"; import ManagedPages from "@/components/managed-pages"; import { useState } from "react"; @@ -31,6 +32,8 @@ export default function DashboardMobile() {
+ + diff --git a/server/migrate.ts b/server/migrate.ts index 26ed145..ca6d248 100644 --- a/server/migrate.ts +++ b/server/migrate.ts @@ -95,6 +95,24 @@ export async function migrate() { ); `); + // posts.generation_status (Reel progress tracking) + await client.query(` + ALTER TABLE "posts" + ADD COLUMN IF NOT EXISTS "generation_status" text; + `); + + // posts.generation_progress + await client.query(` + ALTER TABLE "posts" + ADD COLUMN IF NOT EXISTS "generation_progress" integer DEFAULT 0; + `); + + // posts.generation_error + await client.query(` + ALTER TABLE "posts" + ADD COLUMN IF NOT EXISTS "generation_error" text; + `); + console.log("[Migration] Safe migration completed."); } catch (error) { console.error("[Migration] Error during migration:", error); diff --git a/server/routes/reels.ts b/server/routes/reels.ts index e042c24..73fe8a2 100644 --- a/server/routes/reels.ts +++ b/server/routes/reels.ts @@ -417,7 +417,17 @@ async function processReelBackground( console.log(`🔄 [Background] Starting processing for Post ${postId}`); + // Helper to update generation progress + const updateProgress = async (progress: number, status: string = 'processing') => { + try { + await storage.updatePostGenerationStatus(postId, status, progress); + } catch (e) { + console.error(`⚠️ [Background] Failed to update progress for ${postId}:`, e); + } + }; + try { + await updateProgress(5); // 1. Récupérer le média vidéo source const media = await storage.getMediaById(videoMediaId); if (!media || media.type !== 'video') { @@ -441,6 +451,7 @@ async function processReelBackground( }); // 3. Traiter la vidéo via FFmpeg + await updateProgress(15); const startTime = Date.now(); const ffmpegResult = await ffmpegService.processReelFromUrl(media.originalUrl, { text: overlayText, @@ -461,6 +472,7 @@ async function processReelBackground( } // 4. Upload sur Cloudinary + await updateProgress(65); console.log('☁️ [Background] Uploading to Cloudinary...'); const videoBuffer = Buffer.from(ffmpegResult.videoBase64, 'base64'); const cloudinaryResult = await cloudinaryService.uploadMedia( @@ -486,8 +498,10 @@ async function processReelBackground( // 6. Lier le média traité au Post existant await storage.updatePostMedia(postId, [processedMedia.id]); + await updateProgress(85); // 7. Publier sur les pages + await updateProgress(90); const results: { pageId: string; success: boolean; reelId?: string; error?: string }[] = []; for (const pageId of pageIds) { @@ -562,6 +576,7 @@ async function processReelBackground( await storage.updatePost(postId, { status: finalStatus, }); + await updateProgress(100, 'completed'); console.log(`✅ [Background] Processing complete for Post ${postId}. Status: ${finalStatus}`); @@ -570,6 +585,14 @@ async function processReelBackground( await storage.updatePost(postId, { status: 'failed', }); + try { + await storage.updatePostGenerationStatus( + postId, 'failed', 0, + error?.message || 'Erreur inconnue lors du traitement' + ); + } catch (e) { + console.error(`⚠️ [Background] Failed to update error status for ${postId}:`, e); + } } } @@ -612,8 +635,10 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => { userId: user.id, content: description || overlayText || '', aiGenerated: 'false', - status: scheduledFor ? 'scheduled' : 'draft', // 'draft' servira de 'processing' temporaire + status: scheduledFor ? 'scheduled' : 'draft', scheduledFor: scheduledFor ? new Date(scheduledFor) : undefined, + generationStatus: 'processing', + generationProgress: 0, }); console.log(`✨ Reel Request accepted. Post ID: ${post.id}. Starting background processing.`); @@ -661,3 +686,21 @@ reelsRouter.get('/reels/config', async (req: Request, res: Response) => { res.status(500).json({ error: 'Erreur lors de la récupération de la configuration' }); } }); + +/** + * Reels en cours de génération + * GET /api/reels/ongoing + */ +reelsRouter.get('/reels/ongoing', async (req: Request, res: Response) => { + try { + const user = req.user as User; + const allPosts = await storage.getPosts(user.id); + const ongoing = allPosts.filter( + (p) => p.generationStatus === 'processing' || p.generationStatus === 'pending' + ); + res.json(ongoing); + } catch (error) { + console.error('❌ Error fetching ongoing reels:', error); + res.status(500).json({ error: 'Erreur lors de la récupération des reels en cours' }); + } +}); diff --git a/server/storage.ts b/server/storage.ts index c560a97..9a6f2e6 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -68,6 +68,7 @@ export interface IStorage { getPostWithMedia(id: string): Promise<{ post: Post; media: Media[] } | undefined>; createPost(post: InsertPost): Promise; updatePost(id: string, post: Partial): Promise; + updatePostGenerationStatus(id: string, status: string, progress: number, error?: string): Promise; deletePost(id: string): Promise; updatePostMedia(postId: string, mediaIds: string[]): Promise; @@ -257,6 +258,28 @@ export class DatabaseStorage implements IStorage { await db.delete(posts).where(eq(posts.id, id)); } + async updatePostGenerationStatus( + id: string, + status: string, + progress: number, + error?: string + ): Promise { + const updateData: Record = { + generationStatus: status, + generationProgress: progress, + updatedAt: new Date(), + }; + if (error !== undefined) { + updateData.generationError = error; + } + const [updated] = await db + .update(posts) + .set(updateData) + .where(eq(posts.id, id)) + .returning(); + return updated; + } + async updatePostMedia(postId: string, mediaIds: string[]): Promise { await db.delete(postMedia).where(eq(postMedia.postId, postId)); diff --git a/shared/schema.ts b/shared/schema.ts index 5c37ad5..ea48801 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -163,6 +163,10 @@ export const posts = pgTable("posts", { productInfo: jsonb("product_info"), status: postStatusEnum("status").notNull().default("draft"), scheduledFor: timestamp("scheduled_for"), + // Reel generation tracking + generationStatus: text("generation_status"), // 'pending' | 'processing' | 'completed' | 'failed' + generationProgress: integer("generation_progress").default(0), + generationError: text("generation_error"), createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(), });