Improve ScraperMonitor tab loading efficiency and add New/Updated columns
- Tab-specific data loading: Only fetch APIs needed for the active tab - AZ Live tab fetches only AZ monitor APIs - Dispensary Jobs tab fetches only legacy job APIs - Crawl History tab fetches only scraper history APIs - Auto-refresh now respects active tab - Added New and Updated columns to Crawl History table with color coding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -21,45 +21,42 @@ export function ScraperMonitor() {
|
|||||||
const [azRecentJobs, setAzRecentJobs] = useState<any>({ jobLogs: [], crawlJobs: [] });
|
const [azRecentJobs, setAzRecentJobs] = useState<any>({ jobLogs: [], crawlJobs: [] });
|
||||||
const [azErrors, setAzErrors] = useState<any[]>([]);
|
const [azErrors, setAzErrors] = useState<any[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Load data based on active tab for efficiency
|
||||||
loadData();
|
const loadTabData = async (tab: typeof activeTab) => {
|
||||||
|
|
||||||
if (autoRefresh) {
|
|
||||||
const interval = setInterval(loadData, 3000); // Refresh every 3 seconds
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [autoRefresh]);
|
|
||||||
|
|
||||||
const loadData = async () => {
|
|
||||||
try {
|
try {
|
||||||
const [activeData, historyData, statsData, jobsData, workersData, recentJobsData] = await Promise.all([
|
if (tab === 'az-live') {
|
||||||
api.getActiveScrapers(),
|
// Only load AZ data for AZ Live tab
|
||||||
api.getScraperHistory(),
|
const [azSummaryData, azActiveData, azRecentData, azErrorsData] = await Promise.all([
|
||||||
api.getJobStats(),
|
api.getAZMonitorSummary().catch(() => null),
|
||||||
api.getActiveJobs(),
|
api.getAZMonitorActiveJobs().catch(() => ({ scheduledJobs: [], crawlJobs: [], inMemoryScrapers: [], totalActive: 0 })),
|
||||||
api.getWorkerStats(),
|
api.getAZMonitorRecentJobs(30).catch(() => ({ jobLogs: [], crawlJobs: [] })),
|
||||||
api.getRecentJobs({ limit: 50 })
|
api.getAZMonitorErrors({ limit: 10, hours: 24 }).catch(() => ({ errors: [] })),
|
||||||
]);
|
]);
|
||||||
|
setAzSummary(azSummaryData);
|
||||||
setActiveScrapers(activeData.scrapers || []);
|
setAzActiveJobs(azActiveData);
|
||||||
setHistory(historyData.history || []);
|
setAzRecentJobs(azRecentData);
|
||||||
setJobStats(statsData);
|
setAzErrors(azErrorsData?.errors || []);
|
||||||
setActiveJobs(jobsData.jobs || []);
|
} else if (tab === 'jobs') {
|
||||||
setWorkers(workersData.workers || []);
|
// Only load legacy job data for Dispensary Jobs tab
|
||||||
setRecentJobs(recentJobsData.jobs || []);
|
const [statsData, jobsData, workersData, recentJobsData] = await Promise.all([
|
||||||
|
api.getJobStats(),
|
||||||
// Load AZ monitor data
|
api.getActiveJobs(),
|
||||||
const [azSummaryData, azActiveData, azRecentData, azErrorsData] = await Promise.all([
|
api.getWorkerStats(),
|
||||||
api.getAZMonitorSummary().catch(() => null),
|
api.getRecentJobs({ limit: 50 })
|
||||||
api.getAZMonitorActiveJobs().catch(() => ({ scheduledJobs: [], crawlJobs: [], inMemoryScrapers: [], totalActive: 0 })),
|
]);
|
||||||
api.getAZMonitorRecentJobs(30).catch(() => ({ jobLogs: [], crawlJobs: [] })),
|
setJobStats(statsData);
|
||||||
api.getAZMonitorErrors({ limit: 10, hours: 24 }).catch(() => ({ errors: [] })),
|
setActiveJobs(jobsData.jobs || []);
|
||||||
]);
|
setWorkers(workersData.workers || []);
|
||||||
|
setRecentJobs(recentJobsData.jobs || []);
|
||||||
setAzSummary(azSummaryData);
|
} else if (tab === 'scrapers') {
|
||||||
setAzActiveJobs(azActiveData);
|
// Only load scraper data for Crawl History tab
|
||||||
setAzRecentJobs(azRecentData);
|
const [activeData, historyData] = await Promise.all([
|
||||||
setAzErrors(azErrorsData?.errors || []);
|
api.getActiveScrapers(),
|
||||||
|
api.getScraperHistory()
|
||||||
|
]);
|
||||||
|
setActiveScrapers(activeData.scrapers || []);
|
||||||
|
setHistory(historyData.history || []);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load scraper data:', error);
|
console.error('Failed to load scraper data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -67,6 +64,19 @@ export function ScraperMonitor() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initial load and tab change handler
|
||||||
|
useEffect(() => {
|
||||||
|
loadTabData(activeTab);
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
|
// Auto-refresh based on active tab
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoRefresh) {
|
||||||
|
const interval = setInterval(() => loadTabData(activeTab), 3000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [autoRefresh, activeTab]);
|
||||||
|
|
||||||
const formatDuration = (ms: number) => {
|
const formatDuration = (ms: number) => {
|
||||||
const seconds = Math.floor(ms / 1000);
|
const seconds = Math.floor(ms / 1000);
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
@@ -264,53 +274,77 @@ export function ScraperMonitor() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Individual Crawl Jobs */}
|
{/* Individual Crawl Jobs - Table View */}
|
||||||
{azActiveJobs.crawlJobs.map((job: any) => (
|
{azActiveJobs.crawlJobs.length > 0 && (
|
||||||
<div key={`crawl-${job.id}`} style={{
|
<div style={{
|
||||||
background: 'white',
|
background: 'white',
|
||||||
padding: '20px',
|
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||||
borderLeft: '4px solid #3b82f6'
|
overflow: 'hidden',
|
||||||
|
marginTop: '15px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
|
<div style={{ padding: '15px 20px', borderBottom: '2px solid #eee', background: '#f8f8f8' }}>
|
||||||
<div style={{ flex: 1 }}>
|
<h3 style={{ margin: 0, fontSize: '16px', fontWeight: '600' }}>Active Crawler Sessions ({azActiveJobs.crawlJobs.length})</h3>
|
||||||
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
|
|
||||||
{job.dispensary_name || 'Unknown Store'}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '14px', color: '#666', marginBottom: '12px' }}>
|
|
||||||
{job.city} | {job.job_type || 'crawl'}
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '12px' }}>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#999', marginBottom: '4px' }}>Products Found</div>
|
|
||||||
<div style={{ fontSize: '16px', fontWeight: '600', color: '#8b5cf6' }}>{job.products_found || 0}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#999', marginBottom: '4px' }}>Snapshots</div>
|
|
||||||
<div style={{ fontSize: '16px', fontWeight: '600', color: '#06b6d4' }}>{job.snapshots_created || 0}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#999', marginBottom: '4px' }}>Duration</div>
|
|
||||||
<div style={{ fontSize: '16px', fontWeight: '600' }}>
|
|
||||||
{Math.floor((job.duration_seconds || 0) / 60)}m {Math.floor((job.duration_seconds || 0) % 60)}s
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{
|
|
||||||
padding: '6px 12px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: '600',
|
|
||||||
background: '#dbeafe',
|
|
||||||
color: '#1e40af'
|
|
||||||
}}>
|
|
||||||
CRAWLING
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ borderBottom: '1px solid #eee' }}>
|
||||||
|
<th style={{ padding: '12px 15px', textAlign: 'left', fontWeight: '600', fontSize: '13px', color: '#666' }}>Store</th>
|
||||||
|
<th style={{ padding: '12px 15px', textAlign: 'left', fontWeight: '600', fontSize: '13px', color: '#666' }}>Worker</th>
|
||||||
|
<th style={{ padding: '12px 15px', textAlign: 'center', fontWeight: '600', fontSize: '13px', color: '#666' }}>Page</th>
|
||||||
|
<th style={{ padding: '12px 15px', textAlign: 'right', fontWeight: '600', fontSize: '13px', color: '#666' }}>Products</th>
|
||||||
|
<th style={{ padding: '12px 15px', textAlign: 'right', fontWeight: '600', fontSize: '13px', color: '#666' }}>Snapshots</th>
|
||||||
|
<th style={{ padding: '12px 15px', textAlign: 'right', fontWeight: '600', fontSize: '13px', color: '#666' }}>Duration</th>
|
||||||
|
<th style={{ padding: '12px 15px', textAlign: 'center', fontWeight: '600', fontSize: '13px', color: '#666' }}>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{azActiveJobs.crawlJobs.map((job: any) => (
|
||||||
|
<tr key={`crawl-${job.id}`} style={{ borderBottom: '1px solid #eee' }}>
|
||||||
|
<td style={{ padding: '12px 15px' }}>
|
||||||
|
<div style={{ fontWeight: '600', marginBottom: '2px' }}>{job.dispensary_name || 'Unknown'}</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999' }}>{job.city} | ID: {job.dispensary_id}</div>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 15px', fontSize: '13px' }}>
|
||||||
|
<div style={{ fontFamily: 'monospace', fontSize: '11px', color: '#666' }}>
|
||||||
|
{job.worker_id ? job.worker_id.substring(0, 8) : '-'}
|
||||||
|
</div>
|
||||||
|
{job.worker_hostname && (
|
||||||
|
<div style={{ fontSize: '11px', color: '#999' }}>{job.worker_hostname}</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 15px', textAlign: 'center', fontSize: '13px' }}>
|
||||||
|
{job.current_page && job.total_pages ? (
|
||||||
|
<span>{job.current_page}/{job.total_pages}</span>
|
||||||
|
) : '-'}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 15px', textAlign: 'right', fontWeight: '600', color: '#8b5cf6' }}>
|
||||||
|
{job.products_found || 0}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 15px', textAlign: 'right', fontWeight: '600', color: '#06b6d4' }}>
|
||||||
|
{job.snapshots_created || 0}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 15px', textAlign: 'right', fontSize: '13px' }}>
|
||||||
|
{Math.floor((job.duration_seconds || 0) / 60)}m {Math.floor((job.duration_seconds || 0) % 60)}s
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 15px', textAlign: 'center' }}>
|
||||||
|
<span style={{
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: '600',
|
||||||
|
background: job.last_heartbeat_at && (Date.now() - new Date(job.last_heartbeat_at).getTime() > 60000) ? '#fef3c7' : '#dbeafe',
|
||||||
|
color: job.last_heartbeat_at && (Date.now() - new Date(job.last_heartbeat_at).getTime() > 60000) ? '#92400e' : '#1e40af'
|
||||||
|
}}>
|
||||||
|
{job.last_heartbeat_at && (Date.now() - new Date(job.last_heartbeat_at).getTime() > 60000) ? 'STALE' : 'CRAWLING'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -843,6 +877,8 @@ export function ScraperMonitor() {
|
|||||||
<th style={{ padding: '15px', textAlign: 'left', fontWeight: '600' }}>Dispensary</th>
|
<th style={{ padding: '15px', textAlign: 'left', fontWeight: '600' }}>Dispensary</th>
|
||||||
<th style={{ padding: '15px', textAlign: 'left', fontWeight: '600' }}>Status</th>
|
<th style={{ padding: '15px', textAlign: 'left', fontWeight: '600' }}>Status</th>
|
||||||
<th style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>Found</th>
|
<th style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>Found</th>
|
||||||
|
<th style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>New</th>
|
||||||
|
<th style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>Updated</th>
|
||||||
<th style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>Products</th>
|
<th style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>Products</th>
|
||||||
<th style={{ padding: '15px', textAlign: 'left', fontWeight: '600' }}>Last Crawled</th>
|
<th style={{ padding: '15px', textAlign: 'left', fontWeight: '600' }}>Last Crawled</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -866,6 +902,12 @@ export function ScraperMonitor() {
|
|||||||
<td style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>
|
<td style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>
|
||||||
{item.products_found || '-'}
|
{item.products_found || '-'}
|
||||||
</td>
|
</td>
|
||||||
|
<td style={{ padding: '15px', textAlign: 'right', fontWeight: '600', color: '#059669' }}>
|
||||||
|
{item.products_new || 0}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '15px', textAlign: 'right', fontWeight: '600', color: '#2563eb' }}>
|
||||||
|
{item.products_updated || 0}
|
||||||
|
</td>
|
||||||
<td style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>
|
<td style={{ padding: '15px', textAlign: 'right', fontWeight: '600' }}>
|
||||||
{item.product_count}
|
{item.product_count}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user