Bing Analytics and Insights added, background jobs added, database setup updated, environment setup updated, frontend updated, backend updated.

Onboarding Manager and Router Manager refactored, analytics and background jobs added, database setup updated, environment setup updated, frontend updated, backend updated.
Critical onboarding database migration implemented.
This commit is contained in:
ajaysi
2025-10-18 10:28:15 +05:30
parent 40fb6ac95b
commit 1f087aad4c
69 changed files with 11995 additions and 189 deletions

View File

@@ -0,0 +1,445 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box,
Button,
Card,
CardContent,
Typography,
LinearProgress,
Alert,
Chip,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
CircularProgress,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@mui/material';
import {
PlayArrow,
Stop,
Refresh,
CheckCircle,
Error as ErrorIcon,
Schedule,
ExpandMore,
Analytics,
DataUsage,
} from '@mui/icons-material';
import { apiClient } from '../../api/client';
interface Job {
job_id: string;
job_type: string;
user_id: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
progress: number;
message: string;
created_at: string;
started_at?: string;
completed_at?: string;
result?: any;
error?: string;
}
interface BackgroundJobManagerProps {
siteUrl?: string;
days?: number;
onJobCompleted?: (job: Job) => void;
}
const BackgroundJobManager: React.FC<BackgroundJobManagerProps> = ({
siteUrl = 'https://www.alwrity.com/',
days = 30,
onJobCompleted,
}) => {
const [jobs, setJobs] = useState<Job[]>([]);
const [loading, setLoading] = useState(false);
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
const [jobDialogOpen, setJobDialogOpen] = useState(false);
// Fetch user jobs
const fetchJobs = useCallback(async () => {
try {
const response = await apiClient.get('/api/background-jobs/user-jobs?limit=10');
if (response.data.success) {
setJobs(response.data.data.jobs || []);
}
} catch (error) {
console.error('Error fetching jobs:', error);
}
}, []);
// Create Bing comprehensive insights job
const createComprehensiveInsightsJob = async () => {
setLoading(true);
try {
const response = await apiClient.post(
`/api/background-jobs/bing/comprehensive-insights?site_url=${encodeURIComponent(siteUrl)}&days=${days}`
);
if (response.data.success) {
const jobId = response.data.data.job_id;
console.log('✅ Comprehensive insights job created:', jobId);
// Refresh jobs list
await fetchJobs();
// Show success message
alert(`Background job created successfully! Job ID: ${jobId}\n\nThis will generate comprehensive Bing insights in the background. Check the job status below for progress.`);
}
} catch (error) {
console.error('Error creating comprehensive insights job:', error);
alert('Failed to create background job. Please try again.');
} finally {
setLoading(false);
}
};
// Create Bing data collection job
const createDataCollectionJob = async () => {
setLoading(true);
try {
const response = await apiClient.post(
`/api/background-jobs/bing/data-collection?site_url=${encodeURIComponent(siteUrl)}&days_back=${days}`
);
if (response.data.success) {
const jobId = response.data.data.job_id;
console.log('✅ Data collection job created:', jobId);
// Refresh jobs list
await fetchJobs();
alert(`Background data collection job created successfully! Job ID: ${jobId}\n\nThis will collect fresh data from Bing API in the background.`);
}
} catch (error) {
console.error('Error creating data collection job:', error);
alert('Failed to create data collection job. Please try again.');
} finally {
setLoading(false);
}
};
// Cancel job
const cancelJob = async (jobId: string) => {
try {
const response = await apiClient.post(`/api/background-jobs/cancel/${jobId}`);
if (response.data.success) {
console.log('✅ Job cancelled:', jobId);
await fetchJobs();
alert('Job cancelled successfully');
} else {
alert(response.data.message || 'Failed to cancel job');
}
} catch (error) {
console.error('Error cancelling job:', error);
alert('Failed to cancel job. Please try again.');
}
};
// View job details
const viewJobDetails = async (jobId: string) => {
try {
const response = await apiClient.get(`/api/background-jobs/status/${jobId}`);
if (response.data.success) {
setSelectedJob(response.data.data);
setJobDialogOpen(true);
// Call onJobCompleted if job is completed
if (response.data.data.status === 'completed' && onJobCompleted) {
onJobCompleted(response.data.data);
}
}
} catch (error) {
console.error('Error fetching job details:', error);
alert('Failed to fetch job details');
}
};
// Get status color
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'success';
case 'failed': return 'error';
case 'running': return 'primary';
case 'pending': return 'warning';
case 'cancelled': return 'default';
default: return 'default';
}
};
// Get status icon
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed': return <CheckCircle />;
case 'failed': return <ErrorIcon />;
case 'running': return <CircularProgress size={16} />;
case 'pending': return <Schedule />;
case 'cancelled': return <Stop />;
default: return <Schedule />;
}
};
// Format job type
const formatJobType = (jobType: string) => {
switch (jobType) {
case 'bing_comprehensive_insights': return 'Bing Comprehensive Insights';
case 'bing_data_collection': return 'Bing Data Collection';
case 'analytics_refresh': return 'Analytics Refresh';
default: return jobType;
}
};
// Poll for job updates
useEffect(() => {
fetchJobs();
// Poll every 5 seconds for running jobs
const interval = setInterval(() => {
const hasRunningJobs = jobs.some(job => job.status === 'running' || job.status === 'pending');
if (hasRunningJobs) {
fetchJobs();
}
}, 5000);
return () => clearInterval(interval);
}, [fetchJobs, jobs]);
return (
<Box>
{/* Action Buttons */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Background Job Actions
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Run expensive operations in the background to avoid timeouts and improve user experience.
</Typography>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Button
variant="contained"
startIcon={<Analytics />}
onClick={createComprehensiveInsightsJob}
disabled={loading}
color="primary"
>
{loading ? 'Creating...' : 'Generate Comprehensive Bing Insights'}
</Button>
<Button
variant="outlined"
startIcon={<DataUsage />}
onClick={createDataCollectionJob}
disabled={loading}
color="secondary"
>
{loading ? 'Creating...' : 'Collect Fresh Bing Data'}
</Button>
<Button
variant="outlined"
startIcon={<Refresh />}
onClick={fetchJobs}
disabled={loading}
>
Refresh Jobs
</Button>
</Box>
</CardContent>
</Card>
{/* Jobs List */}
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Background Jobs
</Typography>
{jobs.length === 0 ? (
<Alert severity="info">
No background jobs found. Create a job using the buttons above.
</Alert>
) : (
<List>
{jobs.map((job) => (
<Accordion key={job.job_id} sx={{ mb: 1 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon(job.status)}
<Chip
label={job.status.toUpperCase()}
color={getStatusColor(job.status) as any}
size="small"
/>
</Box>
<Typography variant="body2" sx={{ flexGrow: 1 }}>
{formatJobType(job.job_type)}
</Typography>
<Typography variant="caption" color="text.secondary">
{new Date(job.created_at).toLocaleString()}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ width: '100%' }}>
{/* Progress Bar */}
{(job.status === 'running' || job.status === 'pending') && (
<Box sx={{ mb: 2 }}>
<Typography variant="body2" gutterBottom>
Progress: {job.progress}%
</Typography>
<LinearProgress variant="determinate" value={job.progress} />
</Box>
)}
{/* Job Message */}
<Typography variant="body2" gutterBottom>
<strong>Status:</strong> {job.message}
</Typography>
{/* Job Details */}
<Typography variant="body2" gutterBottom>
<strong>Job ID:</strong> {job.job_id}
</Typography>
{job.started_at && (
<Typography variant="body2" gutterBottom>
<strong>Started:</strong> {new Date(job.started_at).toLocaleString()}
</Typography>
)}
{job.completed_at && (
<Typography variant="body2" gutterBottom>
<strong>Completed:</strong> {new Date(job.completed_at).toLocaleString()}
</Typography>
)}
{job.error && (
<Alert severity="error" sx={{ mt: 1 }}>
<strong>Error:</strong> {job.error}
</Alert>
)}
{/* Action Buttons */}
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
<Button
size="small"
variant="outlined"
onClick={() => viewJobDetails(job.job_id)}
>
View Details
</Button>
{(job.status === 'pending' || job.status === 'running') && (
<Button
size="small"
variant="outlined"
color="error"
onClick={() => cancelJob(job.job_id)}
>
Cancel
</Button>
)}
</Box>
</Box>
</AccordionDetails>
</Accordion>
))}
</List>
)}
</CardContent>
</Card>
{/* Job Details Dialog */}
<Dialog
open={jobDialogOpen}
onClose={() => setJobDialogOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>
Job Details - {selectedJob?.job_id}
</DialogTitle>
<DialogContent>
{selectedJob && (
<Box>
<Typography variant="body1" gutterBottom>
<strong>Type:</strong> {formatJobType(selectedJob.job_type)}
</Typography>
<Typography variant="body1" gutterBottom>
<strong>Status:</strong> {selectedJob.status.toUpperCase()}
</Typography>
<Typography variant="body1" gutterBottom>
<strong>Message:</strong> {selectedJob.message}
</Typography>
<Typography variant="body1" gutterBottom>
<strong>Progress:</strong> {selectedJob.progress}%
</Typography>
<Typography variant="body1" gutterBottom>
<strong>Created:</strong> {new Date(selectedJob.created_at).toLocaleString()}
</Typography>
{selectedJob.started_at && (
<Typography variant="body1" gutterBottom>
<strong>Started:</strong> {new Date(selectedJob.started_at).toLocaleString()}
</Typography>
)}
{selectedJob.completed_at && (
<Typography variant="body1" gutterBottom>
<strong>Completed:</strong> {new Date(selectedJob.completed_at).toLocaleString()}
</Typography>
)}
{selectedJob.result && (
<Box sx={{ mt: 2 }}>
<Typography variant="h6" gutterBottom>
Results:
</Typography>
<pre style={{
backgroundColor: '#f5f5f5',
padding: '16px',
borderRadius: '4px',
overflow: 'auto',
maxHeight: '400px'
}}>
{JSON.stringify(selectedJob.result, null, 2)}
</pre>
</Box>
)}
{selectedJob.error && (
<Alert severity="error" sx={{ mt: 2 }}>
<strong>Error:</strong> {selectedJob.error}
</Alert>
)}
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setJobDialogOpen(false)}>
Close
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default BackgroundJobManager;