import React, { useState, useEffect } from 'react'; import axios from 'axios'; import { ShieldAlert, Layers, Newspaper, History, Plus, Search, Trash2, Edit2, Play, X, AlertTriangle, ExternalLink, Square } from 'lucide-react'; const HOURS_LIST = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); const MINUTES_LIST = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0')); function App() { const [activeTab, setActiveTab] = useState('crawlers'); const [crawlers, setCrawlers] = useState([]); const [ads, setAds] = useState([]); const [logs, setLogs] = useState([]); const [health, setHealth] = useState({ database: 'loading', redis: 'loading' }); const [errorLogModalContent, setErrorLogModalContent] = useState(null); const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', message: '', confirmText: 'حذف', confirmType: 'danger', onConfirm: () => {} }); // Loading states const [loadingCrawlers, setLoadingCrawlers] = useState(true); const [loadingAds, setLoadingAds] = useState(true); const [loadingLogs, setLoadingLogs] = useState(true); // Filters const [filterTaskId, setFilterTaskId] = useState(''); const [filterOnlyFlagged, setFilterOnlyFlagged] = useState(false); const [searchTerm, setSearchTerm] = useState(''); // Modal states const [modalOpen, setModalOpen] = useState(false); const [editingCrawler, setEditingCrawler] = useState(null); // Form state const [formTitle, setFormTitle] = useState(''); const [formUrl, setFormUrl] = useState(''); const [formPrompt, setFormPrompt] = useState(''); const [formInterval, setFormInterval] = useState('15'); const [formTelegram, setFormTelegram] = useState(''); const [formStartHour, setFormStartHour] = useState('08:00'); const [formEndHour, setFormEndHour] = useState('23:00'); const [formIsActive, setFormIsActive] = useState(true); const [formError, setFormError] = useState(''); // Parse hour and minute from form time states const [startH, startM] = (formStartHour || '08:00').split(':'); const [endH, endM] = (formEndHour || '23:00').split(':'); const updateStartHour = (h) => { const parts = (formStartHour || '08:00').split(':'); const m = parts.length > 1 ? parts[1] : '00'; setFormStartHour(`${h}:${m}`); }; const updateStartMinute = (m) => { const parts = (formStartHour || '08:00').split(':'); const h = parts.length > 0 ? parts[0] : '08'; setFormStartHour(`${h}:${m}`); }; const updateEndHour = (h) => { const parts = (formEndHour || '23:00').split(':'); const m = parts.length > 1 ? parts[1] : '00'; setFormEndHour(`${h}:${m}`); }; const updateEndMinute = (m) => { const parts = (formEndHour || '23:00').split(':'); const h = parts.length > 0 ? parts[0] : '23'; setFormEndHour(`${h}:${m}`); }; // Toast alert const [toast, setToast] = useState(null); const showToast = (message, type = 'info') => { setToast({ message, type }); setTimeout(() => { setToast(null); }, 4000); }; // Initial Fetch & Health Check Polling useEffect(() => { fetchHealth(); fetchCrawlers(); const healthInterval = setInterval(fetchHealth, 15000); return () => clearInterval(healthInterval); }, []); // Fetch ads and logs based on active tab and filtering criteria useEffect(() => { if (activeTab === 'ads') { fetchAds(); } else if (activeTab === 'logs') { fetchLogs(); } }, [activeTab, filterTaskId, filterOnlyFlagged]); // Dynamic status polling for active logs useEffect(() => { let logsInterval = null; if (activeTab === 'logs' && logs.some(l => l.status === 'RUNNING')) { logsInterval = setInterval(fetchLogs, 5000); } return () => { if (logsInterval) clearInterval(logsInterval); }; }, [activeTab, logs]); const fetchHealth = async () => { try { const res = await axios.get('/api/health/'); setHealth({ database: res.data.database === 'healthy' ? 'healthy' : 'unhealthy', redis: res.data.redis === 'healthy' ? 'healthy' : 'unhealthy' }); } catch (err) { setHealth({ database: err.response?.data?.database === 'healthy' ? 'healthy' : 'unhealthy', redis: err.response?.data?.redis === 'healthy' ? 'healthy' : 'unhealthy' }); } }; const fetchCrawlers = async () => { setLoadingCrawlers(true); try { const res = await axios.get('/api/crawlers/'); setCrawlers(res.data); } catch (err) { showToast('خطا در دریافت لیست کرالرها', 'danger'); console.error(err); } finally { setLoadingCrawlers(false); } }; const fetchAds = async () => { setLoadingAds(true); try { let url = '/api/ads/'; const params = {}; if (filterTaskId) params.crawl_task = filterTaskId; if (filterOnlyFlagged) params.is_flagged = 'true'; const res = await axios.get(url, { params }); setAds(res.data); } catch (err) { showToast('خطا در دریافت لیست آگهی‌ها', 'danger'); console.error(err); } finally { setLoadingAds(false); } }; const fetchLogs = async () => { setLoadingLogs(true); try { const crawlersList = crawlers.length > 0 ? crawlers : (await axios.get('/api/crawlers/')).data; const promises = crawlersList.map(async (c) => { try { const res = await axios.get(`/api/crawlers/${c.id}/runs/`); return res.data.map(run => ({ ...run, crawler_title: c.title })); } catch (e) { return []; } }); const results = await Promise.all(promises); const merged = results.flat().sort((a, b) => new Date(b.started_at) - new Date(a.started_at)); setLogs(merged); } catch (err) { showToast('خطا در دریافت تاریخچه اجرا', 'danger'); console.error(err); } finally { setLoadingLogs(false); } }; // Switch tabs const switchTab = (tab) => { setActiveTab(tab); }; // Open Edit/Create Modal const openCrawlerModal = (crawler = null) => { setEditingCrawler(crawler); setFormError(''); if (crawler) { setFormTitle(crawler.title); setFormUrl(crawler.divar_url); setFormPrompt(crawler.detection_prompt); setFormInterval(String(crawler.interval_minutes)); setFormTelegram(crawler.telegram_channel_id || ''); setFormStartHour(crawler.start_hour.substring(0, 5)); setFormEndHour(crawler.end_hour.substring(0, 5)); setFormIsActive(crawler.is_active); } else { setFormTitle(''); setFormUrl(''); setFormPrompt(''); setFormInterval('15'); setFormTelegram(''); setFormStartHour('08:00'); setFormEndHour('23:00'); setFormIsActive(true); } setModalOpen(true); }; const closeCrawlerModal = () => { setModalOpen(false); setEditingCrawler(null); }; const handleFormSubmit = async (e) => { e.preventDefault(); setFormError(''); if (!formUrl.startsWith('https://divar.ir/s/')) { setFormError("لینک دیوار باید با 'https://divar.ir/s/' شروع شود"); return; } const payload = { title: formTitle, divar_url: formUrl, detection_prompt: formPrompt, interval_minutes: parseInt(formInterval), start_hour: `${formStartHour}:00`, end_hour: `${formEndHour}:00`, telegram_channel_id: formTelegram || null, is_active: formIsActive }; try { if (editingCrawler) { await axios.put(`/api/crawlers/${editingCrawler.id}/`, payload); showToast('کرالر با موفقیت ویرایش شد', 'success'); } else { await axios.post('/api/crawlers/', payload); showToast('کرالر جدید با موفقیت ایجاد شد', 'success'); } closeCrawlerModal(); fetchCrawlers(); } catch (err) { const errorDetail = err.response?.data ? JSON.stringify(err.response.data) : 'خطا در ثبت اطلاعات'; setFormError(errorDetail); } }; const toggleCrawlerActive = async (crawler) => { try { await axios.patch(`/api/crawlers/${crawler.id}/`, { is_active: !crawler.is_active }); showToast(`کرالر ${!crawler.is_active ? 'فعال' : 'غیرفعال'} شد`, 'info'); fetchCrawlers(); } catch (err) { showToast('خطا در تغییر وضعیت کرالر', 'danger'); } }; const deleteCrawler = (id) => { setConfirmModal({ isOpen: true, title: 'حذف کرالر', message: 'آیا از حذف این کرالر اطمینان دارید؟ تمام سوابق اجرا و آگهی‌های مرتبط نیز حذف خواهند شد.', confirmText: 'حذف کرالر', confirmType: 'danger', onConfirm: async () => { try { await axios.delete(`/api/crawlers/${id}/`); showToast('کرالر با موفقیت حذف شد', 'success'); fetchCrawlers(); } catch (err) { showToast('خطا در حذف کرالر', 'danger'); } } }); }; const triggerCrawler = async (id) => { try { await axios.post(`/api/crawlers/${id}/trigger/`); showToast('درخواست اجرای کرالر با موفقیت ارسال شد', 'success'); if (activeTab === 'logs') { fetchLogs(); } } catch (err) { showToast('خطا در اجرای کرالر', 'danger'); } }; const deleteAd = (id) => { setConfirmModal({ isOpen: true, title: 'حذف آگهی از فید', message: 'آیا از حذف این آگهی از فید اطمینان دارید؟ این عملیات قابل بازگشت نیست.', confirmText: 'حذف آگهی', confirmType: 'danger', onConfirm: async () => { try { await axios.delete(`/api/ads/${id}/`); showToast('آگهی با موفقیت حذف شد', 'success'); setAds(prev => prev.filter(item => item.id !== id)); } catch (err) { showToast('خطا در حذف آگهی', 'danger'); } } }); }; const deleteRun = (id) => { setConfirmModal({ isOpen: true, title: 'حذف سابقه اجرا', message: 'آیا از حذف این رکورد سابقه اجرا اطمینان دارید؟', confirmText: 'حذف رکورد', confirmType: 'danger', onConfirm: async () => { try { await axios.delete(`/api/crawlers/runs/${id}/`); showToast('سابقه اجرا با موفقیت حذف شد', 'success'); setLogs(prev => prev.filter(item => item.id !== id)); } catch (err) { showToast('خطا در حذف سابقه اجرا', 'danger'); } } }); }; const stopRun = (id) => { setConfirmModal({ isOpen: true, title: 'متوقف‌سازی اجرای کرالر', message: 'آیا از متوقف‌سازی این اجرای فعال اطمینان دارید؟', confirmText: 'توقف اجرا', confirmType: 'warning', onConfirm: async () => { try { await axios.post(`/api/crawlers/runs/${id}/stop/`); showToast('اجرای کرالر متوقف شد', 'info'); fetchLogs(); } catch (err) { showToast('خطا در متوقف‌سازی اجرا', 'danger'); } } }); }; // Format date helper const formatDate = (isoStr) => { if (!isoStr) return '-'; const date = new Date(isoStr); return date.toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }) + ' ' + date.toLocaleDateString('fa-IR', { month: '2-digit', day: '2-digit' }); }; // Filter ads on client side based on Search Term const getFilteredAds = () => { if (!searchTerm) return ads; const term = searchTerm.toLowerCase(); return ads.filter(adEval => { const ad = adEval.ad; return ( ad.title?.toLowerCase().includes(term) || ad.description?.toLowerCase().includes(term) || adEval.reason?.toLowerCase().includes(term) ); }); }; return (
{/* Toast Notification */} {toast && (
{toast.message}
)} {/* Sidebar */} {/* Main Content */}
{/* Header */}

{activeTab === 'crawlers' && 'مدیریت کرالرها'} {activeTab === 'ads' && 'فید آگهی‌های دیوار'} {activeTab === 'logs' && 'تاریخچه اجرای کرالرها'}

دیتابیس
ردیس
{/* Viewport */}
{/* TAB 1: Crawlers View */} {activeTab === 'crawlers' && (
{loadingCrawlers ? (

در حال بارگذاری کرالرها...

) : crawlers.length === 0 ? (

هیچ کرالری تعریف نشده است. جهت پایش دیوار، یک کرالر جدید ایجاد کنید.

) : (
{crawlers.map(c => (

{c.title}

لینک فیلتر دیوار {c.divar_url}
پرامپت ارزیابی AI {c.detection_prompt}
فاصله زمانی پایش {c.interval_minutes}m
بازه زمانی مجاز {c.start_hour.substring(0,5)} - {c.end_hour.substring(0,5)}
{c.telegram_channel_id && (
کانال اعلانات تلگرام {c.telegram_channel_id}
)}
))}
)}
)} {/* TAB 2: Ads View */} {activeTab === 'ads' && (
setSearchTerm(e.target.value)} className="w-full bg-black/20 border border-white/5 rounded-lg py-2.5 pr-10 pl-4 text-white text-sm outline-none transition-all duration-200 focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.15)]" />
فقط پرچم‌گذاری شده (AI)
{loadingAds ? (

در حال بارگذاری آگهی‌ها...

) : getFilteredAds().length === 0 ? (

هیچ آگهی با معیارهای فیلتر یافت نشد.

) : (
{getFilteredAds().map(adEval => (
{formatDate(adEval.evaluated_at)} {adEval.ad.price && ( {adEval.ad.price} )}

{adEval.ad.title}

{adEval.ad.description}

{adEval.is_flagged && (
تحلیل هوش مصنوعی (پرچم‌گذاری شده) {adEval.confidence && ( Confidence: {Math.round(adEval.confidence * 100)}% )}

{adEval.reason}

)}
Token: {adEval.ad.divar_token} {adEval.ad.category && ( {adEval.ad.category} )}
))}
)}
)} {/* TAB 3: Logs View */} {activeTab === 'logs' && (
{loadingLogs ? (

در حال دریافت تاریخچه اجرا...

) : logs.length === 0 ? (

هیچ لاگ تاریخی ثبت نشده است.

) : (
{logs.map(run => ( ))}
کرالر شروع اجرا پایان اجرا وضعیت آگهی یافت شده پردازش AI پرچم‌گذاری شده لاگ خطا عملیات
{run.crawler_title} {formatDate(run.started_at)} {formatDate(run.finished_at)} {run.status === 'RUNNING' && 'در حال اجرا'} {run.status === 'SUCCESS' && 'موفق'} {run.status === 'FAILED' && 'ناموفق'} {run.ads_fetched_count} {run.ads_evaluated_count} 0 ? 'text-purple-400 font-bold' : ''}`}> {run.ads_flagged_count} run.error_log && setErrorLogModalContent(run.error_log)} > {run.error_log || '-'}
{run.status === 'RUNNING' && ( )}
)}
)}
{/* Form Dialog Modal */}

{editingCrawler ? 'ویرایش کرالر' : 'تعریف کرالر جدید'}

{formError && (
{formError}
)}
setFormTitle(e.target.value)} placeholder="مثال: رهن کامل فوری نیاوران" className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)]" required />
setFormUrl(e.target.value)} placeholder="https://divar.ir/s/tehran/rent-apartment/niavaran?..." className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)]" required />