- {/* Results Table - Takes 2 columns */}
-
-
-
-
- {/* Preview Panel - Takes 3 columns */}
-
- {selectedFileId && ocrResult ? (
-
- {/* Preview Card */}
-
-
- {/* Stats Grid */}
-
-
-
-
-
-
-
-
-
準確率
-
- {((ocrResult.confidence || 0) * 100).toFixed(1)}%
-
-
-
-
-
-
-
-
-
-
-
-
-
-
處理時間
-
- {(ocrResult.processing_time || 0).toFixed(2)}s
-
-
-
-
-
-
-
-
-
-
-
-
-
-
文字區塊
-
- {ocrResult.json_data?.total_text_regions || 0}
-
-
-
-
-
-
-
- ) : (
-
-
-
-
+ {/* Stats Grid */}
+ {isCompleted && (
+
+
+
+
+
+
-
- {isLoadingResult ? t('common.loading') : '選擇左側檔案以查看詳細結果'}
-
-
-
- )}
+
+
處理時間
+
+ {taskDetail.processing_time_ms ? (taskDetail.processing_time_ms / 1000).toFixed(2) : '0'}s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ )}
+
+ {/* Results Preview */}
+ {isCompleted ? (
+
+
+ 處理結果預覽
+
+
+
+
+
+ ) : taskDetail.status === 'processing' ? (
+
+
+
+ 正在處理中...
+ 請稍候,OCR 處理需要一些時間
+
+
+ ) : taskDetail.status === 'failed' ? (
+
+
+
+ 處理失敗
+ {taskDetail.error_message && (
+ {taskDetail.error_message}
+ )}
+
+
+ ) : (
+
+
+
+ 等待處理
+ 請前往處理頁面啟動 OCR 處理
+
+
+
+ )}
)
}
diff --git a/frontend/src/pages/TaskDetailPage.tsx b/frontend/src/pages/TaskDetailPage.tsx
new file mode 100644
index 0000000..db7d6c3
--- /dev/null
+++ b/frontend/src/pages/TaskDetailPage.tsx
@@ -0,0 +1,346 @@
+import { useParams, useNavigate } from 'react-router-dom'
+import { useTranslation } from 'react-i18next'
+import { useQuery } from '@tanstack/react-query'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import MarkdownPreview from '@/components/MarkdownPreview'
+import { useToast } from '@/components/ui/toast'
+import { apiClientV2 } from '@/services/apiV2'
+import {
+ FileText,
+ Download,
+ AlertCircle,
+ TrendingUp,
+ Clock,
+ Layers,
+ FileJson,
+ Loader2,
+ ArrowLeft,
+ RefreshCw
+} from 'lucide-react'
+import { Badge } from '@/components/ui/badge'
+
+export default function TaskDetailPage() {
+ const { taskId } = useParams<{ taskId: string }>()
+ const { t } = useTranslation()
+ const navigate = useNavigate()
+ const { toast } = useToast()
+
+ // Get task details
+ const { data: taskDetail, isLoading, refetch } = useQuery({
+ queryKey: ['taskDetail', taskId],
+ queryFn: () => apiClientV2.getTask(taskId!),
+ enabled: !!taskId,
+ refetchInterval: (query) => {
+ const data = query.state.data
+ if (!data) return 2000
+ if (data.status === 'completed' || data.status === 'failed') {
+ return false
+ }
+ return 2000 // Poll every 2 seconds for processing tasks
+ },
+ })
+
+ const handleDownloadPDF = async () => {
+ if (!taskId) return
+ try {
+ await apiClientV2.downloadPDF(taskId)
+ toast({
+ title: t('export.exportSuccess'),
+ description: 'PDF 已下載',
+ variant: 'success',
+ })
+ } catch (error: any) {
+ toast({
+ title: t('export.exportError'),
+ description: error.response?.data?.detail || t('errors.networkError'),
+ variant: 'destructive',
+ })
+ }
+ }
+
+ const handleDownloadMarkdown = async () => {
+ if (!taskId) return
+ try {
+ await apiClientV2.downloadMarkdown(taskId)
+ toast({
+ title: t('export.exportSuccess'),
+ description: 'Markdown 已下載',
+ variant: 'success',
+ })
+ } catch (error: any) {
+ toast({
+ title: t('export.exportError'),
+ description: error.response?.data?.detail || t('errors.networkError'),
+ variant: 'destructive',
+ })
+ }
+ }
+
+ const handleDownloadJSON = async () => {
+ if (!taskId) return
+ try {
+ await apiClientV2.downloadJSON(taskId)
+ toast({
+ title: t('export.exportSuccess'),
+ description: 'JSON 已下載',
+ variant: 'success',
+ })
+ } catch (error: any) {
+ toast({
+ title: t('export.exportError'),
+ description: error.response?.data?.detail || t('errors.networkError'),
+ variant: 'destructive',
+ })
+ }
+ }
+
+ const getStatusBadge = (status: string) => {
+ switch (status) {
+ case 'completed':
+ return
已完成
+ case 'processing':
+ return
處理中
+ case 'failed':
+ return
失敗
+ default:
+ return
待處理
+ }
+ }
+
+ const formatDate = (dateStr: string) => {
+ const date = new Date(dateStr)
+ return date.toLocaleString('zh-TW')
+ }
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (!taskDetail) {
+ return (
+
+
+
+
+ 任務不存在
+
+
+ 找不到任務 ID: {taskId}
+
+
+
+
+ )
+ }
+
+ const isCompleted = taskDetail.status === 'completed'
+ const isProcessing = taskDetail.status === 'processing'
+ const isFailed = taskDetail.status === 'failed'
+
+ return (
+
+ {/* Page Header */}
+
+
+
+
+
+
任務詳情
+
+ 任務 ID: {taskId}
+
+
+
+
+
+ {getStatusBadge(taskDetail.status)}
+
+
+
+
+ {/* Task Info Card */}
+
+
+
+
+ 任務資訊
+
+
+
+
+
+
+
檔案名稱
+
{taskDetail.filename || '未知檔案'}
+
+
+
建立時間
+
{formatDate(taskDetail.created_at)}
+
+ {taskDetail.completed_at && (
+
+
完成時間
+
{formatDate(taskDetail.completed_at)}
+
+ )}
+
+
+
+
任務狀態
+ {getStatusBadge(taskDetail.status)}
+
+ {taskDetail.processing_time_ms && (
+
+
處理時間
+
{(taskDetail.processing_time_ms / 1000).toFixed(2)} 秒
+
+ )}
+ {taskDetail.updated_at && (
+
+
最後更新
+
{formatDate(taskDetail.updated_at)}
+
+ )}
+
+
+
+
+
+ {/* Download Options */}
+ {isCompleted && (
+
+
+
+
+ 下載結果
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Error Message */}
+ {isFailed && taskDetail.error_message && (
+
+
+
+
+ 錯誤訊息
+
+
+
+ {taskDetail.error_message}
+
+
+ )}
+
+ {/* Processing Status */}
+ {isProcessing && (
+
+
+
+ 正在處理中...
+ 請稍候,OCR 處理需要一些時間
+
+
+ )}
+
+ {/* Stats Grid (for completed tasks) */}
+ {isCompleted && (
+
+
+
+
+
+
+
+
+
處理時間
+
+ {taskDetail.processing_time_ms ? (taskDetail.processing_time_ms / 1000).toFixed(2) : '0'}s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Result Preview */}
+ {isCompleted && (
+
+
+ 處理結果預覽
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/services/apiV2.ts b/frontend/src/services/apiV2.ts
index 820a085..721912b 100644
--- a/frontend/src/services/apiV2.ts
+++ b/frontend/src/services/apiV2.ts
@@ -47,6 +47,8 @@ class ApiClientV2 {
private userInfo: UserInfo | null = null
private tokenExpiresAt: number | null = null
private refreshTimer: NodeJS.Timeout | null = null
+ private isRefreshing: boolean = false
+ private refreshFailed: boolean = false
constructor() {
this.client = axios.create({
@@ -73,21 +75,41 @@ class ApiClientV2 {
(response) => response,
async (error: AxiosError
) => {
if (error.response?.status === 401) {
+ // If refresh has already failed, don't try again - just redirect
+ if (this.refreshFailed) {
+ console.warn('Refresh already failed, redirecting to login')
+ this.clearAuth()
+ window.location.href = '/login'
+ return Promise.reject(error)
+ }
+
+ // If already refreshing, reject immediately to prevent retry storm
+ if (this.isRefreshing) {
+ console.warn('Token refresh already in progress, rejecting request')
+ return Promise.reject(error)
+ }
+
// Token expired or invalid
const detail = error.response?.data?.detail
if (detail?.includes('Session expired') || detail?.includes('Invalid session')) {
console.warn('Session expired, attempting refresh')
- // Try to refresh token once
+ this.isRefreshing = true
+
try {
await this.refreshToken()
- // Retry the original request
+ this.isRefreshing = false
+
+ // Retry the original request only if refresh succeeded
if (error.config) {
return this.client.request(error.config)
}
} catch (refreshError) {
console.error('Token refresh failed, redirecting to login')
+ this.isRefreshing = false
+ this.refreshFailed = true
this.clearAuth()
window.location.href = '/login'
+ return Promise.reject(error)
}
} else {
this.clearAuth()
@@ -126,6 +148,8 @@ class ApiClientV2 {
this.token = null
this.userInfo = null
this.tokenExpiresAt = null
+ this.isRefreshing = false
+ this.refreshFailed = false
// Clear refresh timer
if (this.refreshTimer) {
diff --git a/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/proposal.md b/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/proposal.md
new file mode 100644
index 0000000..0be756d
--- /dev/null
+++ b/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/proposal.md
@@ -0,0 +1,24 @@
+# Change: Fix V2 API UI Integration Issues
+
+## Why
+After migrating from V1 batch-based architecture to V2 task-based architecture, several UI pages still reference V1 APIs or have incomplete implementations:
+1. Results page (http://127.0.0.1:5173/results) doesn't display task details - uses non-existent V1 `getBatchStatus` API
+2. Task History page markdown downloads produce empty files (0 bytes) - OCR service not generating markdown content
+3. Task History page "View Details" button navigates to `/tasks/{taskId}` route that doesn't exist
+4. Export page (http://127.0.0.1:5173/export) uses non-existent V1 `/api/v2/export` endpoint (404) and lacks multi-task selection
+5. Admin Dashboard page loads but may have permission or API issues
+
+These issues were discovered during testing with task ID: `88c6c2d2-37e1-48fd-a50f-406142987bdf` using file `Henkel-84-1LMISR4 (漢高).pdf`.
+
+## What Changes
+- Migrate ResultsPage from V1 batch API to V2 task API
+- Fix OCR service markdown generation to produce non-empty .md files
+- Add task detail page route and component at `/tasks/:taskId`
+- Update ExportPage to use V2 download endpoints and support multi-task selection
+- Verify and fix Admin Dashboard API integration and permissions
+
+## Impact
+- Affected specs: task-management, result-export
+- Affected code:
+ - Frontend: `src/pages/ResultsPage.tsx`, `src/pages/ExportPage.tsx`, `src/App.tsx` (routes), new `src/pages/TaskDetailPage.tsx`
+ - Backend: `app/services/ocr_service.py` (markdown generation), `app/routers/tasks.py` (download endpoints)
diff --git a/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/specs/result-export/spec.md b/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/specs/result-export/spec.md
new file mode 100644
index 0000000..6df518a
--- /dev/null
+++ b/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/specs/result-export/spec.md
@@ -0,0 +1,46 @@
+# Result Export - Delta Changes
+
+## ADDED Requirements
+
+### Requirement: Export Interface
+The Export page SHALL support downloading OCR results in multiple formats using V2 task APIs.
+
+#### Scenario: Export page uses V2 download endpoints
+- **WHEN** user selects a format and clicks export button
+- **THEN** frontend SHALL call V2 endpoint `/api/v2/tasks/{task_id}/download/{format}`
+- **AND** frontend SHALL NOT call V1 `/api/v2/export` endpoint (which returns 404)
+- **AND** file SHALL download successfully
+
+#### Scenario: Export supports multiple formats
+- **WHEN** user exports a completed task
+- **THEN** system SHALL support downloading as TXT, JSON, Excel, Markdown, and PDF
+- **AND** each format SHALL use correct V2 download endpoint
+- **AND** downloaded files SHALL contain task OCR results
+
+### Requirement: Multi-Task Export Selection
+The Export page SHALL allow users to select and export multiple tasks.
+
+#### Scenario: Select multiple tasks for export
+- **WHEN** Export page loads
+- **THEN** page SHALL display list of user's completed tasks
+- **AND** page SHALL provide checkboxes to select multiple tasks
+- **AND** page SHALL NOT require batch ID from upload store (legacy V1 behavior)
+
+#### Scenario: Export selected tasks
+- **WHEN** user selects multiple tasks and clicks export
+- **THEN** system SHALL download each selected task's results in chosen format
+- **AND** downloaded files SHALL be named distinctly (e.g., `{task_id}_result.{ext}`)
+- **AND** system MAY provide option to download as ZIP archive for multiple files
+
+### Requirement: Export Configuration Persistence
+Export settings (format, thresholds, templates) SHALL apply consistently to V2 task downloads.
+
+#### Scenario: Apply confidence threshold to export
+- **WHEN** user sets confidence threshold to 0.7 and exports
+- **THEN** downloaded results SHALL only include OCR text with confidence >= 0.7
+- **AND** threshold SHALL apply via V2 download endpoint query parameters
+
+#### Scenario: Apply CSS template to PDF export
+- **WHEN** user selects CSS template for PDF format
+- **THEN** downloaded PDF SHALL use selected styling
+- **AND** template SHALL be passed to V2 `/tasks/{id}/download/pdf` endpoint
diff --git a/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/specs/task-management/spec.md b/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/specs/task-management/spec.md
new file mode 100644
index 0000000..7dffa3d
--- /dev/null
+++ b/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/specs/task-management/spec.md
@@ -0,0 +1,51 @@
+# Task Management - Delta Changes
+
+## ADDED Requirements
+
+### Requirement: Task Result Generation
+The OCR service SHALL generate both JSON and Markdown result files for completed tasks with actual content.
+
+#### Scenario: Markdown file contains OCR results
+- **WHEN** a task completes OCR processing successfully
+- **THEN** the generated `.md` file SHALL contain the extracted text in markdown format
+- **AND** the file size SHALL be greater than 0 bytes
+- **AND** the markdown SHALL include headings, paragraphs, and formatting based on OCR layout detection
+
+#### Scenario: Result files stored in task directory
+- **WHEN** OCR processing completes for task ID `88c6c2d2-37e1-48fd-a50f-406142987bdf`
+- **THEN** result files SHALL be stored in `storage/results/88c6c2d2-37e1-48fd-a50f-406142987bdf/`
+- **AND** both `_result.json` and `_result.md` SHALL exist
+- **AND** both files SHALL contain valid OCR output data
+
+### Requirement: Task Detail View
+The frontend SHALL provide a dedicated page for viewing individual task details.
+
+#### Scenario: Navigate to task detail page
+- **WHEN** user clicks "View Details" button on task in Task History page
+- **THEN** browser SHALL navigate to `/tasks/{task_id}`
+- **AND** TaskDetailPage component SHALL render
+
+#### Scenario: Display task information
+- **WHEN** TaskDetailPage loads for a valid task ID
+- **THEN** page SHALL display task metadata (filename, status, processing time, confidence)
+- **AND** page SHALL show markdown preview of OCR results
+- **AND** page SHALL provide download buttons for JSON, Markdown, and PDF formats
+
+#### Scenario: Download from task detail page
+- **WHEN** user clicks download button for a specific format
+- **THEN** browser SHALL download the file using `/api/v2/tasks/{task_id}/download/{format}` endpoint
+- **AND** downloaded file SHALL contain the task's OCR results in requested format
+
+### Requirement: Results Page V2 Migration
+The Results page SHALL use V2 task-based APIs instead of V1 batch APIs.
+
+#### Scenario: Load task results instead of batch
+- **WHEN** Results page loads with a task ID in upload store
+- **THEN** page SHALL call `apiClientV2.getTask(taskId)` to fetch task details
+- **AND** page SHALL NOT call any V1 batch status endpoints
+- **AND** task information SHALL display correctly
+
+#### Scenario: Handle missing task gracefully
+- **WHEN** Results page loads without a task ID
+- **THEN** page SHALL display helpful message directing user to upload page
+- **AND** page SHALL provide button to navigate to `/upload`
diff --git a/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/tasks.md b/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/tasks.md
new file mode 100644
index 0000000..ec75548
--- /dev/null
+++ b/openspec/changes/archive/2025-11-17-fix-v2-api-ui-issues/tasks.md
@@ -0,0 +1,40 @@
+# Implementation Tasks
+
+## 1. Backend Fixes
+- [x] 1.1 Fix markdown generation in OCR service to produce non-empty content
+- [x] 1.2 Verify download endpoints (/tasks/{id}/download/json, markdown, pdf) work correctly
+- [x] 1.3 Verify admin API endpoints (/admin/stats, /admin/users, /admin/users/top) exist and work
+- [x] 1.4 Test markdown file generation with sample task
+
+## 2. Frontend - Results Page Migration
+- [x] 2.1 Remove V1 API imports from ResultsPage.tsx
+- [x] 2.2 Replace `getBatchStatus(batchId)` with V2 task API calls
+- [x] 2.3 Update component to work with task data structure instead of batch
+- [x] 2.4 Test Results page displays task information correctly
+
+## 3. Frontend - Task Detail Page
+- [x] 3.1 Create TaskDetailPage.tsx component
+- [x] 3.2 Add route `/tasks/:taskId` in App.tsx
+- [x] 3.3 Implement task detail view with markdown preview
+- [x] 3.4 Add download buttons for JSON, Markdown, PDF
+- [x] 3.5 Test navigation from Task History page
+
+## 4. Frontend - Export Page Refactor
+- [x] 4.1 Replace V1 `apiClient.exportResults` with V2 download endpoints
+- [x] 4.2 Add task selection UI (replace single batch ID input)
+- [x] 4.3 Implement multi-task download functionality
+- [x] 4.4 Update export button handlers to use V2 APIs
+- [x] 4.5 Test all export formats (TXT, JSON, Excel, Markdown, PDF)
+
+## 5. Admin Dashboard Verification
+- [x] 5.1 Test admin page with admin user credentials
+- [x] 5.2 Verify API calls return data successfully
+- [x] 5.3 Check permission requirements in ProtectedRoute component
+- [x] 5.4 Fix any permission or API issues discovered
+
+## 6. Testing
+- [ ] 6.1 Test complete workflow: Upload → Process → View Results → Download
+- [ ] 6.2 Verify markdown files contain actual OCR content
+- [ ] 6.3 Test task detail navigation and display
+- [ ] 6.4 Test multi-format exports from Export page
+- [ ] 6.5 Test Admin Dashboard with admin account
diff --git a/openspec/specs/result-export/spec.md b/openspec/specs/result-export/spec.md
new file mode 100644
index 0000000..7bd3231
--- /dev/null
+++ b/openspec/specs/result-export/spec.md
@@ -0,0 +1,48 @@
+# result-export Specification
+
+## Purpose
+TBD - created by archiving change fix-v2-api-ui-issues. Update Purpose after archive.
+## Requirements
+### Requirement: Export Interface
+The Export page SHALL support downloading OCR results in multiple formats using V2 task APIs.
+
+#### Scenario: Export page uses V2 download endpoints
+- **WHEN** user selects a format and clicks export button
+- **THEN** frontend SHALL call V2 endpoint `/api/v2/tasks/{task_id}/download/{format}`
+- **AND** frontend SHALL NOT call V1 `/api/v2/export` endpoint (which returns 404)
+- **AND** file SHALL download successfully
+
+#### Scenario: Export supports multiple formats
+- **WHEN** user exports a completed task
+- **THEN** system SHALL support downloading as TXT, JSON, Excel, Markdown, and PDF
+- **AND** each format SHALL use correct V2 download endpoint
+- **AND** downloaded files SHALL contain task OCR results
+
+### Requirement: Multi-Task Export Selection
+The Export page SHALL allow users to select and export multiple tasks.
+
+#### Scenario: Select multiple tasks for export
+- **WHEN** Export page loads
+- **THEN** page SHALL display list of user's completed tasks
+- **AND** page SHALL provide checkboxes to select multiple tasks
+- **AND** page SHALL NOT require batch ID from upload store (legacy V1 behavior)
+
+#### Scenario: Export selected tasks
+- **WHEN** user selects multiple tasks and clicks export
+- **THEN** system SHALL download each selected task's results in chosen format
+- **AND** downloaded files SHALL be named distinctly (e.g., `{task_id}_result.{ext}`)
+- **AND** system MAY provide option to download as ZIP archive for multiple files
+
+### Requirement: Export Configuration Persistence
+Export settings (format, thresholds, templates) SHALL apply consistently to V2 task downloads.
+
+#### Scenario: Apply confidence threshold to export
+- **WHEN** user sets confidence threshold to 0.7 and exports
+- **THEN** downloaded results SHALL only include OCR text with confidence >= 0.7
+- **AND** threshold SHALL apply via V2 download endpoint query parameters
+
+#### Scenario: Apply CSS template to PDF export
+- **WHEN** user selects CSS template for PDF format
+- **THEN** downloaded PDF SHALL use selected styling
+- **AND** template SHALL be passed to V2 `/tasks/{id}/download/pdf` endpoint
+
diff --git a/openspec/specs/task-management/spec.md b/openspec/specs/task-management/spec.md
new file mode 100644
index 0000000..76363da
--- /dev/null
+++ b/openspec/specs/task-management/spec.md
@@ -0,0 +1,53 @@
+# task-management Specification
+
+## Purpose
+TBD - created by archiving change fix-v2-api-ui-issues. Update Purpose after archive.
+## Requirements
+### Requirement: Task Result Generation
+The OCR service SHALL generate both JSON and Markdown result files for completed tasks with actual content.
+
+#### Scenario: Markdown file contains OCR results
+- **WHEN** a task completes OCR processing successfully
+- **THEN** the generated `.md` file SHALL contain the extracted text in markdown format
+- **AND** the file size SHALL be greater than 0 bytes
+- **AND** the markdown SHALL include headings, paragraphs, and formatting based on OCR layout detection
+
+#### Scenario: Result files stored in task directory
+- **WHEN** OCR processing completes for task ID `88c6c2d2-37e1-48fd-a50f-406142987bdf`
+- **THEN** result files SHALL be stored in `storage/results/88c6c2d2-37e1-48fd-a50f-406142987bdf/`
+- **AND** both `_result.json` and `_result.md` SHALL exist
+- **AND** both files SHALL contain valid OCR output data
+
+### Requirement: Task Detail View
+The frontend SHALL provide a dedicated page for viewing individual task details.
+
+#### Scenario: Navigate to task detail page
+- **WHEN** user clicks "View Details" button on task in Task History page
+- **THEN** browser SHALL navigate to `/tasks/{task_id}`
+- **AND** TaskDetailPage component SHALL render
+
+#### Scenario: Display task information
+- **WHEN** TaskDetailPage loads for a valid task ID
+- **THEN** page SHALL display task metadata (filename, status, processing time, confidence)
+- **AND** page SHALL show markdown preview of OCR results
+- **AND** page SHALL provide download buttons for JSON, Markdown, and PDF formats
+
+#### Scenario: Download from task detail page
+- **WHEN** user clicks download button for a specific format
+- **THEN** browser SHALL download the file using `/api/v2/tasks/{task_id}/download/{format}` endpoint
+- **AND** downloaded file SHALL contain the task's OCR results in requested format
+
+### Requirement: Results Page V2 Migration
+The Results page SHALL use V2 task-based APIs instead of V1 batch APIs.
+
+#### Scenario: Load task results instead of batch
+- **WHEN** Results page loads with a task ID in upload store
+- **THEN** page SHALL call `apiClientV2.getTask(taskId)` to fetch task details
+- **AND** page SHALL NOT call any V1 batch status endpoints
+- **AND** task information SHALL display correctly
+
+#### Scenario: Handle missing task gracefully
+- **WHEN** Results page loads without a task ID
+- **THEN** page SHALL display helpful message directing user to upload page
+- **AND** page SHALL provide button to navigate to `/upload`
+