import React, { useState, useEffect } from 'react';
import axios from 'axios';
import {
ShieldAlert,
Layers,
Newspaper,
History,
Plus,
Search,
Trash2,
Edit2,
Play,
X,
AlertTriangle,
RefreshCw,
ExternalLink,
Info
} from 'lucide-react';
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' });
// 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('');
// 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 {
// In Django REST, runs are queried per-crawler. We fetch all crawlers,
// then request runs for each and merge/sort them.
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 = async (id) => {
if (!confirm('آیا از حذف این کرالر اطمینان دارید؟')) return;
try {
await axios.delete(`/api/crawlers/${id}/`);
showToast('کرالر با موفقیت حذف شد', 'success');
fetchCrawlers();
} catch (err) {
showToast('خطا در حذف کرالر', 'danger');
}
};
const triggerCrawler = async (id) => {
try {
const res = await axios.post(`/api/crawlers/${id}/trigger/`);
showToast('درخواست اجرای کرالر با موفقیت ارسال شد', 'success');
if (activeTab === 'logs') {
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 */}
{/* Viewport */}
{/* TAB 1: Crawlers View */}
{activeTab === 'crawlers' && (
{loadingCrawlers ? (
در حال بارگذاری کرالرها...
) : crawlers.length === 0 ? (
هیچ کرالری تعریف نشده است. جهت پایش دیوار، یک کرالر جدید ایجاد کنید.
) : (
{crawlers.map(c => (
لینک فیلتر دیوار
{c.divar_url}
پرامپت ارزیابی AI
{c.detection_prompt}
فاصله زمانی پایش
{c.interval_minutes} دقیقه
بازه زمانی مجاز
{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' && (
{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 && (
اطمینان: {Math.round(adEval.confidence * 100)}٪
)}
{adEval.reason}
)}
توکن: {adEval.ad.divar_token}
{adEval.ad.category && (
{adEval.ad.category}
)}
))}
)}
)}
{/* TAB 3: Logs View */}
{activeTab === 'logs' && (
{loadingLogs ? (
در حال دریافت تاریخچه اجرا...
) : logs.length === 0 ? (
هیچ لاگ تاریخی ثبت نشده است.
) : (
| کرالر |
شروع اجرا |
پایان اجرا |
وضعیت |
آگهی یافت شده |
پردازش AI |
پرچمگذاری شده |
لاگ خطا |
{logs.map(run => (
| {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 ? 'var(--accent)' : 'inherit', fontWeight: run.ads_flagged_count > 0 ? 700 : 'normal' }}>
{run.ads_flagged_count}
|
{run.error_log || '-'}
|
))}
)}
)}
{/* Form Dialog Modal */}
{editingCrawler ? 'ویرایش کرالر' : 'تعریف کرالر جدید'}
);
}
export default App;