@ -9,6 +9,8 @@ import {
calculateRemainingCooldown ,
formatCountdown ,
formatTimestamp ,
calculateElapsedTime ,
formatElapsedTime ,
MATERIALISATION_POLICIES ,
} from '@/src/types/materialisation' ;
@ -23,22 +25,49 @@ export default function MaterialisationStatusPanel({ clubId }: MaterialisationSt
const [ error , setError ] = useState < string | null > ( null ) ;
const [ countdown , setCountdown ] = useState ( 0 ) ;
const [ selectedPolicy , setSelectedPolicy ] = useState < MaterialisationPolicyProfile > ( 'SAFE' ) ;
const [ elapsedTime , setElapsedTime ] = useState ( 0 ) ;
// Poll status on mount and when triggering completes
useEffect ( ( ) = > {
loadStatus ( ) ;
} , [ clubId ] ) ;
// Poll status every 3 seconds while job is running
// Poll status every 2 seconds while job is active (queued/running)
useEffect ( ( ) = > {
if ( ! status || status . status !== 'running' ) return ;
if ( ! status ) return ;
// Poll if legacy status is 'running' OR if last_job is queued/running
const isJobActive =
status . status === 'running' ||
status . last_job ? . status === 'queued' ||
status . last_job ? . status === 'running' ;
if ( ! isJobActive ) return ;
const interval = setInterval ( ( ) = > {
loadStatus ( ) ;
} , 3000 ) ;
} , 2000 ) ; // 2 seconds as per spec
return ( ) = > clearInterval ( interval ) ;
} , [ status ? . status , status ? . last_job ? . status ] ) ;
// Update elapsed time every second for running jobs
useEffect ( ( ) = > {
if ( ! status ? . last_job ? . started_at || status . last_job . status !== 'running' ) {
setElapsedTime ( 0 ) ;
return ;
}
const elapsed = calculateElapsedTime ( status . last_job . started_at ) ;
setElapsedTime ( elapsed ) ;
const interval = setInterval ( ( ) = > {
const newElapsed = calculateElapsedTime ( status . last_job ! . started_at ! ) ;
setElapsedTime ( newElapsed ) ;
} , 1000 ) ;
return ( ) = > clearInterval ( interval ) ;
} , [ status ? . status ] ) ;
} , [ status ? . last_job? . started_at , status ? . last_job ? . status] ) ;
// Update countdown every second when rate limited
useEffect ( ( ) = > {
@ -139,8 +168,9 @@ export default function MaterialisationStatusPanel({ clubId }: MaterialisationSt
if ( ! status ) return null ;
// Determine panel state
const hasActiveJob = status . last_job ? . status === 'queued' || status . last_job ? . status === 'running' ;
const isRateLimited = ! status . rate_limit . can_trigger && countdown > 0 ;
const canTrigger = status . rate_limit . can_trigger && status . status !== 'running' && ! triggering;
const canTrigger = status . rate_limit . can_trigger && status . status !== 'running' && ! hasActiveJob && ! triggering;
// Render status panel
return (
@ -244,8 +274,84 @@ export default function MaterialisationStatusPanel({ clubId }: MaterialisationSt
< / div >
) }
{ /* Active Job Status (queued/running) */ }
{ status . last_job && ( status . last_job . status === 'queued' || status . last_job . status === 'running' ) && (
< div className = "space-y-3" >
< div className = "bg-blue-50 border border-blue-200 rounded-lg p-4" >
< div className = "flex items-start space-x-3" >
< Loader2 className = "w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5 animate-spin" / >
< div className = "flex-1" >
{ status . last_job . status === 'queued' && (
< p className = "text-sm text-blue-900 font-semibold" >
Job queued , waiting to start . . .
< / p >
) }
{ status . last_job . status === 'running' && (
< >
< p className = "text-sm text-blue-900 font-semibold" >
Generating slots . . . ( { formatElapsedTime ( elapsedTime ) } )
< / p >
< p className = "text-xs text-blue-700 mt-1" >
Policy : { status . last_job . policy_profile } · Horizon : { status . last_job . horizon_days } days
< / p >
< / >
) }
< / div >
< / div >
< / div >
< / div >
) }
{ /* Completed Job Results */ }
{ status . last_job && status . last_job . status === 'completed' && status . last_job . result && ! hasActiveJob && (
< div className = "space-y-3" >
< div className = "bg-green-50 border border-green-200 rounded-lg p-4" >
< div className = "flex items-start space-x-3" >
< CheckCircle className = "w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" / >
< div className = "flex-1" >
< p className = "text-sm text-green-900 font-semibold mb-2" >
✅ Generated { status . last_job . result . created } slots
{ status . last_job . result . cancelled > 0 && ` ( ${ status . last_job . result . cancelled } cancelled) ` }
{ status . last_job . result . errors . length > 0 && ` ( ${ status . last_job . result . errors . length } errors) ` }
< / p >
< div className = "text-xs text-green-700 space-y-1" >
{ status . last_job . result . updated > 0 && < div > • Updated : { status . last_job . result . updated } < / div > }
{ status . last_job . result . moved > 0 && < div > • Moved : { status . last_job . result . moved } < / div > }
{ status . last_job . result . skipped > 0 && < div > • Skipped : { status . last_job . result . skipped } < / div > }
{ status . last_job . completed_at && (
< div className = "mt-2 text-slate-600" > Completed : { formatTimestamp ( status . last_job . completed_at ) } < / div >
) }
< / div >
< / div >
< / div >
< / div >
< / div >
) }
{ /* Failed Job Status */ }
{ status . last_job && status . last_job . status === 'failed' && ! hasActiveJob && (
< div className = "space-y-3" >
< div className = "bg-red-50 border border-red-200 rounded-lg p-4" >
< div className = "flex items-start space-x-3" >
< AlertCircle className = "w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" / >
< div className = "flex-1" >
< p className = "text-sm text-red-900 font-semibold mb-1" >
❌ Job failed
< / p >
{ status . last_job . error && (
< p className = "text-sm text-red-700" > { status . last_job . error } < / p >
) }
{ status . last_job . failed_at && (
< div className = "text-xs text-slate-600 mt-2" > Failed : { formatTimestamp ( status . last_job . failed_at ) } < / div >
) }
< / div >
< / div >
< / div >
< / div >
) }
{ /* Rate limited state */ }
{ isRateLimited && (
{ isRateLimited && ! hasActiveJob && (
< div className = "space-y-3" >
{ status . last_success_at && (
< div className = "text-sm text-slate-700" >