geocrop-platform./apps/web/src/StatusMonitor.tsx

156 lines
5.3 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import axios from 'axios';
interface StatusMonitorProps {
jobId: string;
onJobFinished: (jobId: string, results: any) => void;
}
const API_ENDPOINT = 'https://api.portfolio.techarvest.co.zw';
// Pipeline stages with their relative weights/progress and baseline durations (in seconds)
const STAGES: Record<string, { progress: number; label: string; eta: number }> = {
'queued': { progress: 5, label: 'In Queue', eta: 30 },
'fetch_stac': { progress: 15, label: 'Fetching Satellite Imagery', eta: 120 },
'build_features': { progress: 40, label: 'Computing Spectral Indices', eta: 180 },
'load_dw': { progress: 50, label: 'Loading Base Classification', eta: 45 },
'infer': { progress: 75, label: 'Running Ensemble Prediction', eta: 90 },
'smooth': { progress: 85, label: 'Refining Results', eta: 30 },
'export_cog': { progress: 95, label: 'Generating Output Maps', eta: 20 },
'upload': { progress: 98, label: 'Finalizing Storage', eta: 10 },
'finished': { progress: 100, label: 'Complete', eta: 0 },
'done': { progress: 100, label: 'Complete', eta: 0 },
'failed': { progress: 0, label: 'Job Failed', eta: 0 }
};
const StatusMonitor: React.FC<StatusMonitorProps> = ({ jobId, onJobFinished }) => {
const [status, setStatus] = useState<string>('queued');
const [countdown, setCountdown] = useState<number>(0);
useEffect(() => {
let interval: number;
const checkStatus = async () => {
try {
const response = await axios.get(`${API_ENDPOINT}/jobs/${jobId}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = response.data;
const currentStatus = data.status || 'queued';
setStatus(currentStatus);
// Reset countdown whenever stage changes
if (STAGES[currentStatus]) {
setCountdown(STAGES[currentStatus].eta);
}
if (currentStatus === 'finished' || currentStatus === 'done') {
clearInterval(interval);
const result = data.result || data.outputs;
const roi = data.roi;
onJobFinished(jobId, { result, roi });
} else if (currentStatus === 'failed') {
clearInterval(interval);
}
} catch (err) {
console.error('Status check failed:', err);
}
};
interval = window.setInterval(checkStatus, 5000);
checkStatus();
return () => clearInterval(interval);
}, [jobId, onJobFinished]);
// Handle local countdown timer
useEffect(() => {
const timer = setInterval(() => {
setCountdown(prev => (prev > 0 ? prev - 1 : 0));
}, 1000);
return () => clearInterval(timer);
}, []);
const stageInfo = STAGES[status] || { progress: 0, label: 'Processing...', eta: 60 };
const progress = stageInfo.progress;
const getStatusColor = () => {
if (status === 'finished' || status === 'done') return '#28a745';
if (status === 'failed') return '#dc3545';
return '#1a73e8';
};
return (
<div style={{
fontSize: '12px',
padding: '12px',
background: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e9ecef',
marginBottom: '10px',
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontWeight: '700', color: '#202124' }}>Job: {jobId.substring(0, 8)}</span>
<span style={{
textTransform: 'uppercase',
fontSize: '9px',
background: getStatusColor(),
color: 'white',
padding: '2px 6px',
borderRadius: '4px',
fontWeight: 'bold'
}}>
{status}
</span>
</div>
<div style={{ color: '#5f6368', fontSize: '11px', marginBottom: '8px' }}>
Current Step: <strong>{stageInfo.label}</strong>
</div>
<div style={{ position: 'relative', height: '8px', background: '#e8eaed', borderRadius: '4px', overflow: 'hidden', marginBottom: '8px' }}>
<div style={{
width: `${progress}%`,
height: '100%',
background: getStatusColor(),
transition: 'width 0.5s ease-in-out'
}} />
</div>
{(status !== 'finished' && status !== 'done' && status !== 'failed') ? (
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#1a73e8', fontSize: '10px', fontWeight: '600' }}>
<span>Estimated Progress: {progress}%</span>
<span>ETA: {Math.floor(countdown / 60)}m {countdown % 60}s</span>
</div>
) : (status === 'finished' || status === 'done') ? (
<button
onClick={() => {
// Trigger overlay again if needed
window.location.hash = `job=${jobId}`;
// This is a bit of a hack, better to handle in parent but we call onJobFinished again
// to ensure parent has the data
}}
style={{
width: '100%',
padding: '5px',
background: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '11px',
fontWeight: 'bold'
}}>
Overlay on Map
</button>
) : null}
</div>
);
};
export default StatusMonitor;