Skip to content

Commit e78d5a0

Browse files
committed
refactor(frontend/training): replace SSE with polling for job updates
Updated the TrainingPage component to utilize polling instead of Server-Sent Events (SSE) for real-time job status updates. This change improves compatibility with AWS Amplify's Lambda@Edge limitations. Modified files (3): - frontend/app/training/[jobId]/page.tsx: Updated to use polling - frontend/lib/useJobPolling.ts: Added custom hook for polling - frontend/lib/useJobSSE.ts: Removed SSE hook
1 parent 76de7d5 commit e78d5a0

6 files changed

Lines changed: 148 additions & 363 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Supports: sklearn models (Random Forest, Extra Trees) and LightGBM
2424
- Verification step ensures exported ONNX model is valid
2525

26-
- **Real-time Training Updates (SSE)** - Server-Sent Events for live status updates
27-
- New `/api/jobs/[jobId]/stream` SSE endpoint in Next.js
28-
- Custom `useJobSSE` hook with automatic fallback to polling
29-
- Visual SSE connection status indicator on training page
30-
- Updates every 3 seconds instead of 5 seconds polling
31-
- Graceful handling of connection errors and timeouts
32-
3326
- **Model Comparison** - Side-by-side comparison of training runs
3427
- New `/compare` page for comparing up to 4 models
3528
- Metrics comparison table with best model highlighting
3629
- Feature importance comparison with visual bars
3730
- Quick job selection from completed training history
3831
- Link to compare from history page header
3932

33+
- **Training Run Tags & Notes** - Organize and annotate your training experiments
34+
- Add custom tags (up to 10) and notes (up to 1000 chars) to any training job
35+
- `JobMetadataEditor` component with edit/save workflow
36+
- New `PATCH /jobs/{job_id}` API endpoint for metadata updates
37+
- History page displays tags and supports filtering by tag
38+
- Compact tag display mode in tables
39+
40+
- **Enhanced Error Handling** - Comprehensive error boundaries for better UX
41+
- Global `error.tsx` with network error detection and troubleshooting tips
42+
- Custom `not-found.tsx` (404) page with quick navigation links
43+
- `global-error.tsx` for critical root layout failures
44+
- Route-specific error boundaries for `/training/[jobId]` and `/results/[jobId]`
45+
- Development mode error details display
46+
- Root `loading.tsx` with animated spinner
47+
48+
- **Dataset Preview Enhancements** - Rich dataset visualization on configure page
49+
- `ColumnStatsDisplay` component with dataset overview stats
50+
- Visual column type distribution (numeric vs categorical)
51+
- Missing values warning with affected columns list
52+
- Selected column details with unique ratio visualization
53+
4054
### Fixed
4155
- **Problem Type Detection** - Regression datasets were incorrectly classified as classification
4256
- Fixed heuristic logic: now requires BOTH integer-like values AND low cardinality

frontend/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,15 @@ frontend/
4949
│ ├── training/[jobId]/ # Training status page
5050
│ ├── results/[jobId]/ # Results & download page
5151
│ ├── compare/ # Model comparison page (v1.1.0)
52-
│ ├── history/ # Training history list
53-
│ └── api/jobs/[jobId]/stream/ # SSE endpoint for real-time updates
52+
│ └── history/ # Training history list
5453
├── components/
5554
│ ├── FileUpload.tsx # Drag & drop upload component
5655
│ ├── Header.tsx # Navigation header with theme toggle
5756
│ └── ThemeToggle.tsx # Dark/light mode switcher
5857
├── lib/
5958
│ ├── api.ts # API client functions
6059
│ ├── utils.ts # Utility functions
61-
│ └── useJobSSE.ts # SSE hook with polling fallback
60+
│ └── useJobPolling.ts # Job status polling hook
6261
├── public/ # Static assets
6362
└── package.json
6463
```

frontend/app/api/jobs/[jobId]/stream/route.ts

Lines changed: 0 additions & 128 deletions
This file was deleted.

frontend/app/training/[jobId]/page.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useEffect } from 'react';
44
import { useRouter, useParams } from 'next/navigation';
55
import { JobDetails } from '@/lib/api';
6-
import { useJobSSE } from '@/lib/useJobSSE';
6+
import { useJobPolling } from '@/lib/useJobPolling';
77
import { getStatusColor, getStatusIcon, formatDuration, formatDateTime } from '@/lib/utils';
88
import Header from '@/components/Header';
99

@@ -12,10 +12,9 @@ export default function TrainingPage() {
1212
const params = useParams();
1313
const jobId = params.jobId as string;
1414

15-
// Use SSE for real-time updates
16-
const { job, isLoading, error, sseStatus, isUsingSSE } = useJobSSE(jobId, {
15+
// Use polling for real-time updates
16+
const { job, isLoading, error } = useJobPolling(jobId, {
1717
enabled: true,
18-
fallbackToPolling: true,
1918
pollingInterval: 5000,
2019
onComplete: (completedJob: JobDetails) => {
2120
// Redirect to results when job completes
@@ -137,17 +136,9 @@ export default function TrainingPage() {
137136
<p className="text-sm text-gray-600 dark:text-gray-400 text-center mt-2">
138137
This may take a few minutes depending on dataset size and time budget
139138
</p>
140-
{/* SSE Status Indicator */}
141-
<div className="flex items-center justify-center gap-2 mt-2">
142-
<span className={`inline-block w-2 h-2 rounded-full ${
143-
sseStatus === 'connected' ? 'bg-green-500 animate-pulse' :
144-
sseStatus === 'connecting' ? 'bg-yellow-500 animate-pulse' :
145-
sseStatus === 'error' ? 'bg-red-500' : 'bg-gray-400'
146-
}`}></span>
147-
<span className="text-xs text-gray-500 dark:text-gray-500">
148-
{isUsingSSE ? 'Real-time updates' : 'Polling for updates'}{sseStatus}
149-
</span>
150-
</div>
139+
<p className="text-xs text-gray-500 dark:text-gray-500 text-center mt-1">
140+
Updating every 5 seconds
141+
</p>
151142
</div>
152143

153144
{/* Job Info */}

frontend/lib/useJobPolling.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Custom hook for job status polling
3+
*
4+
* Polls the backend API for job status updates at a configurable interval.
5+
* Automatically stops polling when job reaches a terminal state (completed/failed).
6+
*
7+
* Note: This hook was originally designed for SSE but SSE is not compatible
8+
* with AWS Amplify's Lambda@Edge (30s timeout). Polling is the reliable solution
9+
* for serverless platforms.
10+
*/
11+
12+
import { useState, useEffect, useCallback, useRef } from 'react';
13+
import { JobDetails, getJobDetails } from './api';
14+
15+
interface UseJobPollingOptions {
16+
/** Enable polling (default: true) */
17+
enabled?: boolean;
18+
/** Polling interval in ms (default: 5000) */
19+
pollingInterval?: number;
20+
/** Callback when job completes */
21+
onComplete?: (job: JobDetails) => void;
22+
/** Callback when job fails */
23+
onError?: (error: string) => void;
24+
}
25+
26+
interface UseJobPollingResult {
27+
/** Current job data */
28+
job: JobDetails | null;
29+
/** Loading state (initial fetch) */
30+
isLoading: boolean;
31+
/** Error message if any */
32+
error: string | null;
33+
/** Manually refresh job status */
34+
refresh: () => Promise<void>;
35+
}
36+
37+
export function useJobPolling(
38+
jobId: string,
39+
options: UseJobPollingOptions = {}
40+
): UseJobPollingResult {
41+
const {
42+
enabled = true,
43+
pollingInterval = 5000,
44+
onComplete,
45+
onError,
46+
} = options;
47+
48+
const [job, setJob] = useState<JobDetails | null>(null);
49+
const [isLoading, setIsLoading] = useState(true);
50+
const [error, setError] = useState<string | null>(null);
51+
52+
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
53+
const onCompleteRef = useRef(onComplete);
54+
const onErrorRef = useRef(onError);
55+
56+
// Keep callbacks up to date
57+
onCompleteRef.current = onComplete;
58+
onErrorRef.current = onError;
59+
60+
// Cleanup function
61+
const cleanup = useCallback(() => {
62+
if (pollingIntervalRef.current) {
63+
clearInterval(pollingIntervalRef.current);
64+
pollingIntervalRef.current = null;
65+
}
66+
}, []);
67+
68+
const refresh = useCallback(async () => {
69+
try {
70+
const jobData = await getJobDetails(jobId);
71+
setJob(jobData);
72+
setError(null);
73+
} catch (err) {
74+
setError(err instanceof Error ? err.message : 'Failed to fetch job');
75+
}
76+
}, [jobId]);
77+
78+
// Setup polling
79+
useEffect(() => {
80+
if (!enabled || !jobId) return;
81+
82+
const poll = async () => {
83+
try {
84+
const jobData = await getJobDetails(jobId);
85+
setJob(jobData);
86+
setIsLoading(false);
87+
setError(null);
88+
89+
// Handle terminal states
90+
if (jobData.status === 'completed') {
91+
cleanup();
92+
onCompleteRef.current?.(jobData);
93+
} else if (jobData.status === 'failed') {
94+
cleanup();
95+
onErrorRef.current?.(jobData.error_message || 'Training failed');
96+
}
97+
} catch (err) {
98+
setIsLoading(false);
99+
setError(err instanceof Error ? err.message : 'Failed to fetch job');
100+
// Don't stop polling on transient errors
101+
}
102+
};
103+
104+
// Initial fetch
105+
poll();
106+
107+
// Start polling
108+
pollingIntervalRef.current = setInterval(poll, pollingInterval);
109+
110+
return cleanup;
111+
}, [jobId, enabled, pollingInterval, cleanup]);
112+
113+
return {
114+
job,
115+
isLoading,
116+
error,
117+
refresh,
118+
};
119+
}

0 commit comments

Comments
 (0)