"use client"; import { useState, useEffect, useTransition } from "react"; import { Database, AlertTriangle, Link2, Plus, Loader2, ArrowRight, RefreshCw, } from "lucide-react"; import { toast } from "sonner"; import { createDestinationViaWorker, setGlobalDestination, rebuildPackageDatabase, } from "../actions"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { GlobalDestination, ChannelRow } from "@/lib/telegram/admin-queries"; interface DestinationCardProps { destination: GlobalDestination; channels?: ChannelRow[]; } type CreateState = | { phase: "idle" } | { phase: "creating"; requestId?: string } | { phase: "done"; title: string; telegramId: string } | { phase: "error"; message: string }; type RebuildState = | { phase: "idle" } | { phase: "running"; requestId: string } | { phase: "done"; created: number; skipped: number; scanned: number } | { phase: "error"; message: string }; interface RebuildProgress { status: string; messagesScanned: number; documentsFound: number; packagesCreated: number; packagesSkipped: number; error?: string; } export function DestinationCard({ destination, channels = [] }: DestinationCardProps) { const [isPending, startTransition] = useTransition(); const [createOpen, setCreateOpen] = useState(false); const [title, setTitle] = useState("dragonsstash db"); const [createState, setCreateState] = useState({ phase: "idle" }); const [selectedChannelId, setSelectedChannelId] = useState(""); const [rebuildState, setRebuildState] = useState({ phase: "idle" }); const [rebuildProgress, setRebuildProgress] = useState(null); // Channels that can be assigned as destination (SOURCE channels only, exclude current destination) const assignableChannels = channels.filter( (c) => c.type === "SOURCE" && c.id !== destination?.id ); // Poll for worker result when creating useEffect(() => { if (createState.phase !== "creating" || !createState.requestId) return; let mounted = true; const requestId = createState.requestId; const poll = async () => { for (let i = 0; i < 60; i++) { await new Promise((r) => setTimeout(r, 2000)); if (!mounted) return; try { const res = await fetch( `/api/telegram/worker-request?requestId=${requestId}` ); if (!res.ok) continue; const data = await res.json(); if (data.status === "COMPLETED" && data.result) { if (mounted) { setCreateState({ phase: "done", title: data.result.title, telegramId: data.result.telegramId, }); toast.success(`Telegram group "${data.result.title}" created and set as destination!`); setCreateOpen(false); // Refresh the page to show the new destination window.location.reload(); } return; } else if (data.status === "FAILED") { if (mounted) { setCreateState({ phase: "error", message: data.error || "Worker failed to create the group", }); } return; } } catch { // Network blip — keep polling } } if (mounted) { setCreateState({ phase: "error", message: "Timed out waiting for the worker" }); } }; poll(); return () => { mounted = false; }; }, [createState]); // Poll for rebuild progress useEffect(() => { if (rebuildState.phase !== "running") return; let mounted = true; const requestId = rebuildState.requestId; const poll = async () => { for (let i = 0; i < 300; i++) { await new Promise((r) => setTimeout(r, 2000)); if (!mounted) return; try { const res = await fetch( `/api/telegram/worker-request?requestId=${requestId}` ); if (!res.ok) continue; const data = await res.json(); // Update live progress from resultJson if (data.result && typeof data.result === "object") { if (mounted) setRebuildProgress(data.result as RebuildProgress); } if (data.status === "COMPLETED" && data.result) { const result = data.result as RebuildProgress; if (mounted) { setRebuildState({ phase: "done", created: result.packagesCreated, skipped: result.packagesSkipped, scanned: result.messagesScanned, }); setRebuildProgress(null); toast.success( `Rebuild complete: ${result.packagesCreated} packages restored, ${result.packagesSkipped} skipped` ); } return; } else if (data.status === "FAILED") { if (mounted) { setRebuildState({ phase: "error", message: data.error || "Rebuild failed", }); setRebuildProgress(null); } return; } } catch { // Network blip — keep polling } } if (mounted) { setRebuildState({ phase: "error", message: "Timed out waiting for rebuild" }); setRebuildProgress(null); } }; poll(); return () => { mounted = false; }; }, [rebuildState]); const handleRebuild = () => { startTransition(async () => { const result = await rebuildPackageDatabase(); if (result.success) { setRebuildState({ phase: "running", requestId: result.data.requestId }); setRebuildProgress(null); toast.info("Rebuild started — scanning destination channel..."); } else { toast.error(result.error ?? "Failed to start rebuild"); } }); }; const handleCreate = () => { if (!title.trim()) return; startTransition(async () => { const result = await createDestinationViaWorker(title.trim()); if (result.success) { setCreateState({ phase: "creating", requestId: result.data.requestId }); } else { setCreateState({ phase: "error", message: result.error ?? "Unknown error" }); } }); }; const handleAssignExisting = () => { if (!selectedChannelId) return; startTransition(async () => { const result = await setGlobalDestination(selectedChannelId); if (result.success) { toast.success("Channel set as destination!"); setCreateOpen(false); setSelectedChannelId(""); } else { toast.error(result.error ?? "Failed to set destination"); } }); }; const handleOpenChange = (open: boolean) => { setCreateOpen(open); if (!open) { // Reset state when closing (unless actively creating) if (createState.phase !== "creating") { setCreateState({ phase: "idle" }); } setSelectedChannelId(""); } }; if (!destination) { return ( <>

No destination channel configured

Create a private Telegram group that all accounts will write archives to. Requires at least one authenticated account.

); } return ( <>

{destination.title}

DESTINATION
ID: {destination.telegramId} {destination.inviteLink && ( Invite link active )}
{/* Rebuild progress */} {rebuildState.phase === "running" && rebuildProgress && (
Rebuilding package database...
{rebuildProgress.messagesScanned} {" "} messages scanned {rebuildProgress.documentsFound} {" "} archives found {rebuildProgress.packagesCreated} {" "} restored {rebuildProgress.packagesSkipped} {" "} skipped
)} {rebuildState.phase === "done" && (
Rebuild complete: {rebuildState.created} packages restored,{" "} {rebuildState.skipped} skipped ({rebuildState.scanned} messages scanned)
)} {rebuildState.phase === "error" && (
Rebuild failed: {rebuildState.message}
)}
); } function DestinationDialog({ open, onOpenChange, title, setTitle, onSubmitCreate, createState, isPending, assignableChannels, selectedChannelId, setSelectedChannelId, onSubmitAssign, }: { open: boolean; onOpenChange: (open: boolean) => void; title: string; setTitle: (v: string) => void; onSubmitCreate: () => void; createState: CreateState; isPending: boolean; assignableChannels: ChannelRow[]; selectedChannelId: string; setSelectedChannelId: (v: string) => void; onSubmitAssign: () => void; }) { const isCreating = createState.phase === "creating"; const hasAssignable = assignableChannels.length > 0; return ( Set Destination Channel Choose an existing channel or create a new private group. All accounts will write archives to this destination. {isCreating ? (

Creating Telegram group...

This may take a few seconds

) : ( Use Existing Create New {createState.phase === "error" && (

{createState.message}

)}

The selected channel will become the destination. All accounts will be linked as writers automatically.

{createState.phase === "error" && (

{createState.message}

)}
setTitle(e.target.value)} />

A new private Telegram group will be created using one of your authenticated accounts. You can rename it later in Telegram.

)}
); }