Skip to content

Commit b980e97

Browse files
committed
fixed duration
1 parent 5a0ccdc commit b980e97

3 files changed

Lines changed: 41 additions & 12 deletions

File tree

api/src/routes/training.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,17 @@ pub async fn post_metrics(
194194

195195
// Check for training completion (progress >= 100)
196196
if event.metric_name == "progress" && event.value >= 100.0 {
197+
// Mark job as completed with final timestamp
198+
sqlx::query(
199+
"UPDATE jobs SET status = 'completed', completed_at = COALESCE(completed_at, NOW()), updated_at = NOW() WHERE id = $1 AND status != 'completed'"
200+
)
201+
.bind(job_id)
202+
.execute(&state.db)
203+
.await
204+
.ok();
205+
206+
state.metrics.remove(&job_id).await;
207+
197208
// Look up the job owner to notify them
198209
let owner: Option<(Uuid,)> = sqlx::query_as("SELECT created_by FROM jobs WHERE id = $1")
199210
.bind(job_id)

model-runner/python/runner.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ def update_job_status(conn, job_id, status, error_message=None, progress=None):
6060
with conn.cursor() as cur:
6161
if status == "running":
6262
cur.execute(
63-
"UPDATE jobs SET status = 'running', started_at = NOW(), "
63+
"UPDATE jobs SET status = 'running', "
64+
"started_at = COALESCE(started_at, NOW()), "
6465
"updated_at = NOW() WHERE id = %s",
6566
(job_id,),
6667
)

web/src/app/training/page.tsx

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,12 @@ function timeSince(date: string | null): string {
5858
function 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

9396
export 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

Comments
 (0)