@@ -58,9 +58,12 @@ function timeSince(date: string | null): string {
5858function duration ( start : string | null , end : string | null ) : string {
5959 if ( ! start ) return "—" ;
6060 const endTime = end ? new Date ( end ) . getTime ( ) : Date . now ( ) ;
61- const diff = endTime - new Date ( start ) . getTime ( ) ;
62- const mins = Math . floor ( diff / 60000 ) ;
63- if ( mins < 60 ) return `${ mins } m` ;
61+ const diff = Math . max ( 0 , endTime - new Date ( start ) . getTime ( ) ) ;
62+ const totalSec = Math . floor ( diff / 1000 ) ;
63+ if ( totalSec < 60 ) return `${ totalSec } s` ;
64+ const mins = Math . floor ( totalSec / 60 ) ;
65+ const secs = totalSec % 60 ;
66+ if ( mins < 60 ) return `${ mins } m ${ secs } s` ;
6467 const hrs = Math . floor ( mins / 60 ) ;
6568 return `${ hrs } h ${ mins % 60 } m` ;
6669}
@@ -92,7 +95,8 @@ const statusColors: Record<string, string> = {
9295
9396export default function TrainingPage ( ) {
9497 const { selectedProjectId } = useProjectFilter ( ) ;
95- const [ jobs , setJobs ] = useState < TrainingJob [ ] > ( [ ] ) ;
98+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99+ const [ rawJobs , setRawJobs ] = useState < any [ ] > ( [ ] ) ;
96100 const [ loading , setLoading ] = useState ( true ) ;
97101 const [ error , setError ] = useState < string | null > ( null ) ;
98102 const [ stopJobId , setStopJobId ] = useState < string | null > ( null ) ;
@@ -101,21 +105,34 @@ export default function TrainingPage() {
101105 const [ newJobTier , setNewJobTier ] = useState ( "" ) ;
102106 const [ submitting , setSubmitting ] = useState ( false ) ;
103107 const [ models , setModels ] = useState < { id : string ; name : string ; framework : string } [ ] > ( [ ] ) ;
108+ const [ , setTick ] = useState ( 0 ) ; // force re-render for live durations
104109
105- const fetchJobs = ( ) => {
106- setLoading ( true ) ;
107- setError ( null ) ;
110+ const fetchJobs = ( initial = false ) => {
111+ if ( initial ) { setLoading ( true ) ; setError ( null ) ; }
108112 api . getFiltered < any [ ] > ( "/training/jobs" , selectedProjectId )
109- . then ( ( data ) => setJobs ( data . map ( mapJob ) ) )
110- . catch ( ( err ) => setError ( err instanceof Error ? err . message : "Failed to load training jobs" ) )
111- . finally ( ( ) => setLoading ( false ) ) ;
113+ . then ( ( data ) => setRawJobs ( data ) )
114+ . catch ( ( err ) => { if ( initial ) setError ( err instanceof Error ? err . message : "Failed to load training jobs" ) ; } )
115+ . finally ( ( ) => { if ( initial ) setLoading ( false ) ; } ) ;
112116 } ;
113117
118+ // Map raw jobs on every render so Date.now() stays fresh for running-job durations
119+ const jobs = rawJobs . map ( mapJob ) ;
120+
114121 useEffect ( ( ) => {
115- fetchJobs ( ) ;
122+ fetchJobs ( true ) ;
116123 api . getFiltered < { id : string ; name : string ; framework : string } [ ] > ( "/models" , selectedProjectId ) . then ( setModels ) . catch ( ( ) => { } ) ;
117124 } , [ selectedProjectId ] ) ;
118125
126+ // Poll every 5s when there are active jobs + tick every second for live durations
127+ const hasActiveJobs = rawJobs . some ( ( j : any ) => j . status === "running" || j . status === "pending" ) ;
128+
129+ useEffect ( ( ) => {
130+ if ( ! hasActiveJobs ) return ;
131+ const poll = setInterval ( ( ) => fetchJobs ( false ) , 5000 ) ;
132+ const tick = setInterval ( ( ) => setTick ( t => t + 1 ) , 1000 ) ;
133+ return ( ) => { clearInterval ( poll ) ; clearInterval ( tick ) ; } ;
134+ } , [ hasActiveJobs , selectedProjectId ] ) ;
135+
119136 const handleNewJob = async ( ) => {
120137 if ( ! newModelId ) { toast . error ( "Select a model" ) ; return ; }
121138 setSubmitting ( true ) ;
0 commit comments