🔧 Fix authentication and document upload issues
## What was done: ✅ Fixed Firebase Admin initialization to use default credentials for Firebase Functions ✅ Updated frontend to use correct Firebase Functions URL (was using Cloud Run URL) ✅ Added comprehensive debugging to authentication middleware ✅ Added debugging to file upload middleware and CORS handling ✅ Added debug buttons to frontend for troubleshooting authentication ✅ Enhanced error handling and logging throughout the stack ## Current issues: ❌ Document upload still returns 400 Bad Request despite authentication working ❌ GET requests work fine (200 OK) but POST upload requests fail ❌ Frontend authentication is working correctly (valid JWT tokens) ❌ Backend authentication middleware is working (rejects invalid tokens) ❌ CORS is configured correctly and allowing requests ## Root cause analysis: - Authentication is NOT the issue (tokens are valid, GET requests work) - The problem appears to be in the file upload handling or multer configuration - Request reaches the server but fails during upload processing - Need to identify exactly where in the upload pipeline the failure occurs ## TODO next steps: 1. 🔍 Check Firebase Functions logs after next upload attempt to see debugging output 2. 🔍 Verify if request reaches upload middleware (look for '�� Upload middleware called' logs) 3. 🔍 Check if file validation is triggered (look for '🔍 File filter called' logs) 4. 🔍 Identify specific error in upload pipeline (multer, file processing, etc.) 5. 🔍 Test with smaller file or different file type to isolate issue 6. 🔍 Check if issue is with Firebase Functions file size limits or timeout 7. 🔍 Verify multer configuration and file handling in Firebase Functions environment ## Technical details: - Frontend: https://cim-summarizer.web.app - Backend: https://us-central1-cim-summarizer.cloudfunctions.net/api - Authentication: Firebase Auth with JWT tokens (working correctly) - File upload: Multer with memory storage for immediate GCS upload - Debug buttons available in production frontend for troubleshooting
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
VITE_API_BASE_URL=https://api-y56ccs6wva-uc.a.run.app
|
||||
VITE_API_BASE_URL=https://us-central1-cim-summarizer.cloudfunctions.net/api
|
||||
VITE_FIREBASE_API_KEY=AIzaSyBoV04YHkbCSUIU6sXki57um4xNsvLV_jY
|
||||
VITE_FIREBASE_AUTH_DOMAIN=cim-summarizer.firebaseapp.com
|
||||
VITE_FIREBASE_PROJECT_ID=cim-summarizer
|
||||
|
||||
@@ -4,13 +4,80 @@
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
"**/node_modules/**",
|
||||
"src/**",
|
||||
"*.test.ts",
|
||||
"*.test.js",
|
||||
"jest.config.js",
|
||||
"tsconfig.json",
|
||||
".eslintrc.js",
|
||||
"vite.config.ts",
|
||||
"tailwind.config.js",
|
||||
"postcss.config.js"
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"source": "**/*.js",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=31536000, immutable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "**/*.css",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=31536000, immutable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "**/*.html",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache, no-store, must-revalidate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache, no-store, must-revalidate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "**/*.@(jpg|jpeg|gif|png|svg|webp|ico)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=31536000, immutable"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
],
|
||||
"cleanUrls": true,
|
||||
"trailingSlash": false
|
||||
},
|
||||
"emulators": {
|
||||
"hosting": {
|
||||
"port": 5000
|
||||
},
|
||||
"ui": {
|
||||
"enabled": true,
|
||||
"port": 4000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"deploy:firebase": "npm run build && firebase deploy --only hosting",
|
||||
"deploy:preview": "npm run build && firebase hosting:channel:deploy preview",
|
||||
"emulator": "firebase emulators:start --only hosting",
|
||||
"emulator:ui": "firebase emulators:start --only hosting --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
|
||||
@@ -7,8 +7,10 @@ import DocumentUpload from './components/DocumentUpload';
|
||||
import DocumentList from './components/DocumentList';
|
||||
import DocumentViewer from './components/DocumentViewer';
|
||||
import Analytics from './components/Analytics';
|
||||
import UploadMonitoringDashboard from './components/UploadMonitoringDashboard';
|
||||
import LogoutButton from './components/LogoutButton';
|
||||
import { documentService } from './services/documentService';
|
||||
import { documentService, GCSErrorHandler, GCSError } from './services/documentService';
|
||||
import { debugAuth, testAPIAuth } from './utils/authDebug';
|
||||
|
||||
import {
|
||||
Home,
|
||||
@@ -17,7 +19,8 @@ import {
|
||||
BarChart3,
|
||||
Plus,
|
||||
Search,
|
||||
TrendingUp
|
||||
TrendingUp,
|
||||
Activity
|
||||
} from 'lucide-react';
|
||||
import { cn } from './utils/cn';
|
||||
|
||||
@@ -28,7 +31,7 @@ const Dashboard: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [viewingDocument, setViewingDocument] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'documents' | 'upload' | 'analytics'>('overview');
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'documents' | 'upload' | 'analytics' | 'monitoring'>('overview');
|
||||
|
||||
// Map backend status to frontend status
|
||||
const mapBackendStatus = (backendStatus: string): string => {
|
||||
@@ -61,7 +64,7 @@ const Dashboard: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('https://us-central1-cim-summarizer.cloudfunctions.net/api/api/documents', {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/documents`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -116,7 +119,7 @@ const Dashboard: React.FC = () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await fetch(`https://us-central1-cim-summarizer.cloudfunctions.net/api/api/documents/${documentId}/progress`, {
|
||||
const response = await fetch(`https://us-central1-cim-summarizer.cloudfunctions.net/api/documents/${documentId}/progress`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -246,7 +249,14 @@ const Dashboard: React.FC = () => {
|
||||
console.log('Download completed');
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Failed to download document. Please try again.');
|
||||
|
||||
// Handle GCS-specific errors
|
||||
if (GCSErrorHandler.isGCSError(error)) {
|
||||
const gcsError = error as GCSError;
|
||||
alert(`Download failed: ${GCSErrorHandler.getErrorMessage(gcsError)}`);
|
||||
} else {
|
||||
alert('Failed to download document. Please try again.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -281,6 +291,15 @@ const Dashboard: React.FC = () => {
|
||||
setViewingDocument(null);
|
||||
};
|
||||
|
||||
// Debug functions
|
||||
const handleDebugAuth = async () => {
|
||||
await debugAuth();
|
||||
};
|
||||
|
||||
const handleTestAPIAuth = async () => {
|
||||
await testAPIAuth();
|
||||
};
|
||||
|
||||
const filteredDocuments = documents.filter(doc =>
|
||||
doc.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
doc.originalName.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
@@ -368,7 +387,20 @@ const Dashboard: React.FC = () => {
|
||||
<span className="text-sm text-white">
|
||||
Welcome, {user?.name || user?.email}
|
||||
</span>
|
||||
<LogoutButton variant="link" />
|
||||
{/* Debug buttons - show in production for troubleshooting */}
|
||||
<button
|
||||
onClick={handleDebugAuth}
|
||||
className="bg-yellow-500 hover:bg-yellow-600 text-white px-3 py-1 rounded text-sm"
|
||||
>
|
||||
Debug Auth
|
||||
</button>
|
||||
<button
|
||||
onClick={handleTestAPIAuth}
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded text-sm"
|
||||
>
|
||||
Test API
|
||||
</button>
|
||||
<LogoutButton variant="button" className="bg-error-500 hover:bg-error-600 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,6 +459,18 @@ const Dashboard: React.FC = () => {
|
||||
<TrendingUp className="h-4 w-4 mr-2" />
|
||||
Analytics
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('monitoring')}
|
||||
className={cn(
|
||||
'flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200',
|
||||
activeTab === 'monitoring'
|
||||
? 'border-primary-600 text-primary-700'
|
||||
: 'border-transparent text-gray-500 hover:text-primary-600 hover:border-primary-300'
|
||||
)}
|
||||
>
|
||||
<Activity className="h-4 w-4 mr-2" />
|
||||
Monitoring
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -615,6 +659,10 @@ const Dashboard: React.FC = () => {
|
||||
{activeTab === 'analytics' && (
|
||||
<Analytics />
|
||||
)}
|
||||
|
||||
{activeTab === 'monitoring' && (
|
||||
<UploadMonitoringDashboard />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { Upload, FileText, X, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import { Upload, FileText, X, CheckCircle, AlertCircle, Cloud } from 'lucide-react';
|
||||
import { cn } from '../utils/cn';
|
||||
import { documentService } from '../services/documentService';
|
||||
import { documentService, GCSErrorHandler, GCSError } from '../services/documentService';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
interface UploadedFile {
|
||||
@@ -14,6 +14,10 @@ interface UploadedFile {
|
||||
progress: number;
|
||||
error?: string;
|
||||
documentId?: string; // Real document ID from backend
|
||||
// GCS-specific fields
|
||||
gcsError?: boolean;
|
||||
storageType?: 'gcs' | 'local';
|
||||
gcsUrl?: string;
|
||||
}
|
||||
|
||||
interface DocumentUploadProps {
|
||||
@@ -136,14 +140,33 @@ const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
);
|
||||
} else {
|
||||
console.error('Upload failed:', error);
|
||||
|
||||
// Handle GCS-specific errors
|
||||
let errorMessage = 'Upload failed';
|
||||
let isGCSError = false;
|
||||
|
||||
if (GCSErrorHandler.isGCSError(error)) {
|
||||
errorMessage = GCSErrorHandler.getErrorMessage(error as GCSError);
|
||||
isGCSError = true;
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
setUploadedFiles(prev =>
|
||||
prev.map(f =>
|
||||
f.id === uploadedFile.id
|
||||
? { ...f, status: 'error', error: error instanceof Error ? error.message : 'Upload failed' }
|
||||
? {
|
||||
...f,
|
||||
status: 'error',
|
||||
error: errorMessage,
|
||||
// Add GCS error indicator
|
||||
...(isGCSError && { gcsError: true })
|
||||
}
|
||||
: f
|
||||
)
|
||||
);
|
||||
onUploadError?.(error instanceof Error ? error.message : 'Upload failed');
|
||||
|
||||
onUploadError?.(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
// Clean up the abort controller
|
||||
@@ -171,7 +194,7 @@ const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
|
||||
const checkProgress = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://us-central1-cim-summarizer.cloudfunctions.net/api/api/documents/${documentId}/progress`, {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/documents/${documentId}/progress`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -274,18 +297,20 @@ const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: UploadedFile['status'], error?: string) => {
|
||||
const getStatusText = (status: UploadedFile['status'], error?: string, gcsError?: boolean) => {
|
||||
switch (status) {
|
||||
case 'uploading':
|
||||
return 'Uploading...';
|
||||
return 'Uploading to Google Cloud Storage...';
|
||||
case 'uploaded':
|
||||
return 'Uploaded ✓';
|
||||
return 'Uploaded to GCS ✓';
|
||||
case 'processing':
|
||||
return 'Processing with Optimized Agentic RAG...';
|
||||
case 'completed':
|
||||
return 'Completed ✓';
|
||||
case 'error':
|
||||
return error === 'Upload cancelled' ? 'Cancelled' : 'Error';
|
||||
if (error === 'Upload cancelled') return 'Cancelled';
|
||||
if (gcsError) return 'GCS Error';
|
||||
return 'Error';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -326,7 +351,7 @@ const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
Drag and drop PDF files here, or click to browse
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Maximum file size: 50MB • Supported format: PDF • Automatic Optimized Agentic RAG Processing
|
||||
Maximum file size: 50MB • Supported format: PDF • Stored securely in Google Cloud Storage • Automatic Optimized Agentic RAG Processing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -354,7 +379,7 @@ const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-success-800">Upload Complete</h4>
|
||||
<p className="text-sm text-success-700 mt-1">
|
||||
Files have been uploaded successfully! You can now navigate away from this page.
|
||||
Files have been uploaded successfully to Google Cloud Storage! You can now navigate away from this page.
|
||||
Processing will continue in the background using Optimized Agentic RAG and you can check the status in the Documents tab.
|
||||
</p>
|
||||
</div>
|
||||
@@ -401,8 +426,12 @@ const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
<div className="flex items-center space-x-1">
|
||||
{getStatusIcon(file.status)}
|
||||
<span className="text-xs text-gray-600">
|
||||
{getStatusText(file.status, file.error)}
|
||||
{getStatusText(file.status, file.error, file.gcsError)}
|
||||
</span>
|
||||
{/* GCS indicator */}
|
||||
{file.storageType === 'gcs' && (
|
||||
<Cloud className="h-3 w-3 text-blue-500" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Remove Button */}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../utils/cn';
|
||||
import CIMReviewTemplate from './CIMReviewTemplate';
|
||||
import LogoutButton from './LogoutButton';
|
||||
|
||||
|
||||
interface ExtractedData {
|
||||
@@ -306,6 +307,9 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
<p className="text-sm text-gray-600">{documentName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<LogoutButton variant="button" className="bg-error-500 hover:bg-error-600 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ const ProcessingProgress: React.FC<ProcessingProgressProps> = ({
|
||||
|
||||
const pollProgress = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://us-central1-cim-summarizer.cloudfunctions.net/api/api/documents/${documentId}/progress`, {
|
||||
const response = await fetch(`https://us-central1-cim-summarizer.cloudfunctions.net/api/documents/${documentId}/progress`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -40,7 +40,7 @@ const QueueStatus: React.FC<QueueStatusProps> = ({ refreshTrigger }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('https://us-central1-cim-summarizer.cloudfunctions.net/api/api/documents/queue/status', {
|
||||
const response = await fetch('https://us-central1-cim-summarizer.cloudfunctions.net/api/documents/queue/status', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
530
frontend/src/components/UploadMonitoringDashboard.tsx
Normal file
530
frontend/src/components/UploadMonitoringDashboard.tsx
Normal file
@@ -0,0 +1,530 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
RefreshCw,
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
interface UploadMetrics {
|
||||
totalUploads: number;
|
||||
successfulUploads: number;
|
||||
failedUploads: number;
|
||||
successRate: number;
|
||||
averageProcessingTime: number;
|
||||
totalProcessingTime: number;
|
||||
uploadsByHour: { [hour: string]: number };
|
||||
errorsByType: { [errorType: string]: number };
|
||||
errorsByStage: { [stage: string]: number };
|
||||
fileSizeDistribution: {
|
||||
small: number;
|
||||
medium: number;
|
||||
large: number;
|
||||
};
|
||||
processingTimeDistribution: {
|
||||
fast: number;
|
||||
normal: number;
|
||||
slow: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UploadHealthStatus {
|
||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
successRate: number;
|
||||
averageProcessingTime: number;
|
||||
recentErrors: Array<{
|
||||
id: string;
|
||||
userId: string;
|
||||
fileInfo: {
|
||||
originalName: string;
|
||||
size: number;
|
||||
mimetype: string;
|
||||
};
|
||||
status: string;
|
||||
stage?: string;
|
||||
error?: {
|
||||
message: string;
|
||||
code?: string;
|
||||
type: string;
|
||||
};
|
||||
processingTime?: number;
|
||||
timestamp: string;
|
||||
correlationId?: string;
|
||||
}>;
|
||||
recommendations: string[];
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface RealTimeStats {
|
||||
activeUploads: number;
|
||||
uploadsLastMinute: number;
|
||||
uploadsLastHour: number;
|
||||
currentSuccessRate: number;
|
||||
}
|
||||
|
||||
interface ErrorAnalysis {
|
||||
topErrorTypes: Array<{ type: string; count: number; percentage: number }>;
|
||||
topErrorStages: Array<{ stage: string; count: number; percentage: number }>;
|
||||
errorTrends: Array<{ hour: string; errorCount: number; totalCount: number }>;
|
||||
}
|
||||
|
||||
interface DashboardData {
|
||||
metrics: UploadMetrics;
|
||||
healthStatus: UploadHealthStatus;
|
||||
realTimeStats: RealTimeStats;
|
||||
errorAnalysis: ErrorAnalysis;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const UploadMonitoringDashboard: React.FC = () => {
|
||||
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [timeRange, setTimeRange] = useState('24');
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/monitoring/dashboard?hours=${timeRange}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch dashboard data');
|
||||
}
|
||||
const result = await response.json();
|
||||
setDashboardData(result.data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
}, [timeRange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh) return;
|
||||
|
||||
const interval = setInterval(fetchDashboardData, 30000); // Refresh every 30 seconds
|
||||
return () => clearInterval(interval);
|
||||
}, [autoRefresh, timeRange]);
|
||||
|
||||
const getHealthStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return 'bg-green-500';
|
||||
case 'degraded':
|
||||
return 'bg-yellow-500';
|
||||
case 'unhealthy':
|
||||
return 'bg-red-500';
|
||||
default:
|
||||
return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getHealthStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return <CheckCircle className="h-5 w-5 text-green-500" />;
|
||||
case 'degraded':
|
||||
return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
|
||||
case 'unhealthy':
|
||||
return <AlertCircle className="h-5 w-5 text-red-500" />;
|
||||
default:
|
||||
return <Activity className="h-5 w-5 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (ms: number) => {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
return `${(ms / 60000).toFixed(1)}m`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (loading && !dashboardData) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<RefreshCw className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-2">Loading dashboard data...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<AlertCircle className="h-4 w-4 text-red-600 mr-2" />
|
||||
<span className="text-red-800">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dashboardData) {
|
||||
return (
|
||||
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 mr-2" />
|
||||
<span className="text-yellow-800">No dashboard data available</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { metrics, healthStatus, realTimeStats, errorAnalysis } = dashboardData;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Upload Pipeline Monitoring</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Real-time monitoring and analytics for document upload processing
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<select
|
||||
value={timeRange}
|
||||
onChange={(e) => setTimeRange(e.target.value)}
|
||||
className="w-32 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="1">1 Hour</option>
|
||||
<option value="6">6 Hours</option>
|
||||
<option value="24">24 Hours</option>
|
||||
<option value="168">7 Days</option>
|
||||
</select>
|
||||
<button
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
autoRefresh
|
||||
? 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'
|
||||
}`}
|
||||
onClick={() => setAutoRefresh(!autoRefresh)}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 inline ${autoRefresh ? 'animate-spin' : ''}`} />
|
||||
Auto Refresh
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 text-sm font-medium bg-white text-gray-700 border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onClick={fetchDashboardData}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2 inline" />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Health Status */}
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900 flex items-center space-x-2">
|
||||
{getHealthStatusIcon(healthStatus.status)}
|
||||
<span>System Health Status</span>
|
||||
<span className={`ml-2 px-2 py-1 text-xs font-medium rounded-full ${getHealthStatusColor(healthStatus.status)} text-white`}>
|
||||
{healthStatus.status.toUpperCase()}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{(healthStatus.successRate * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Success Rate</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{formatTime(healthStatus.averageProcessingTime)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Avg Processing Time</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{healthStatus.recentErrors.length}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Recent Errors</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{healthStatus.recommendations.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="font-semibold mb-2">Recommendations:</h4>
|
||||
<ul className="space-y-1">
|
||||
{healthStatus.recommendations.map((rec, index) => (
|
||||
<li key={index} className="text-sm text-muted-foreground flex items-start">
|
||||
<AlertCircle className="h-4 w-4 mr-2 mt-0.5 text-yellow-500" />
|
||||
{rec}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Real-time Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200 p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Activity className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">Active Uploads</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{realTimeStats.activeUploads}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200 p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm font-medium">Last Minute</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{realTimeStats.uploadsLastMinute}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200 p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<TrendingUp className="h-4 w-4 text-purple-500" />
|
||||
<span className="text-sm font-medium">Last Hour</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{realTimeStats.uploadsLastHour}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200 p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm font-medium">Success Rate</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{(realTimeStats.currentSuccessRate * 100).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Metrics */}
|
||||
<div className="space-y-4">
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button className="border-b-2 border-blue-500 py-2 px-1 text-sm font-medium text-blue-600">
|
||||
Overview
|
||||
</button>
|
||||
<button className="border-b-2 border-transparent py-2 px-1 text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
Error Analysis
|
||||
</button>
|
||||
<button className="border-b-2 border-transparent py-2 px-1 text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
Performance
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900">Upload Statistics</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Success Rate</span>
|
||||
<span>{(metrics.successRate * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${metrics.successRate * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{metrics.successfulUploads}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Successful</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{metrics.failedUploads}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900">File Size Distribution</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Small (< 1MB)</span>
|
||||
<span className="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 rounded-full">{metrics.fileSizeDistribution.small}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Medium (1MB - 10MB)</span>
|
||||
<span className="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 rounded-full">{metrics.fileSizeDistribution.medium}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Large (> 10MB)</span>
|
||||
<span className="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 rounded-full">{metrics.fileSizeDistribution.large}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900">Top Error Types</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="space-y-3">
|
||||
{errorAnalysis.topErrorTypes.map((error, index) => (
|
||||
<div key={index} className="flex justify-between items-center">
|
||||
<span className="text-sm truncate">{error.type}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium">{error.count}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({error.percentage.toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900">Top Error Stages</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="space-y-3">
|
||||
{errorAnalysis.topErrorStages.map((stage, index) => (
|
||||
<div key={index} className="flex justify-between items-center">
|
||||
<span className="text-sm truncate">{stage.stage}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium">{stage.count}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({stage.percentage.toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{healthStatus.recentErrors.length > 0 && (
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900">Recent Errors</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="space-y-3">
|
||||
{healthStatus.recentErrors.slice(0, 5).map((error) => (
|
||||
<div key={error.id} className="border rounded-lg p-3">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className="font-medium text-sm">{error.fileInfo.originalName}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(error.timestamp).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mb-1">
|
||||
Stage: {error.stage || 'Unknown'}
|
||||
</div>
|
||||
{error.error && (
|
||||
<div className="text-sm text-red-600">
|
||||
{error.error.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900">Processing Time Distribution</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Fast (< 30s)</span>
|
||||
<span className="px-2 py-1 text-xs font-medium bg-gray-100 text-green-600 rounded-full">
|
||||
{metrics.processingTimeDistribution.fast}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Normal (30s - 5m)</span>
|
||||
<span className="px-2 py-1 text-xs font-medium bg-gray-100 text-blue-600 rounded-full">
|
||||
{metrics.processingTimeDistribution.normal}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Slow (> 5m)</span>
|
||||
<span className="px-2 py-1 text-xs font-medium bg-gray-100 text-red-600 rounded-full">
|
||||
{metrics.processingTimeDistribution.slow}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow rounded-lg border border-gray-200">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900">Performance Metrics</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Average Processing Time</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatTime(metrics.averageProcessingTime)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Total Processing Time</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatTime(metrics.totalProcessingTime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last Updated */}
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Last updated: {new Date(dashboardData.timestamp).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadMonitoringDashboard;
|
||||
@@ -1,6 +1,6 @@
|
||||
// Frontend environment configuration
|
||||
export const config = {
|
||||
apiBaseUrl: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
apiBaseUrl: import.meta.env.VITE_API_BASE_URL || '',
|
||||
appName: import.meta.env.VITE_APP_NAME || 'CIM Document Processor',
|
||||
maxFileSize: parseInt(import.meta.env.VITE_MAX_FILE_SIZE || '104857600'), // 100MB
|
||||
allowedFileTypes: (import.meta.env.VITE_ALLOWED_FILE_TYPES || 'application/pdf').split(','),
|
||||
|
||||
@@ -13,8 +13,12 @@ const apiClient = axios.create({
|
||||
// Add auth token to requests
|
||||
apiClient.interceptors.request.use(async (config) => {
|
||||
const token = await authService.getToken();
|
||||
console.log('🔐 Auth interceptor - Token available:', !!token);
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
console.log('🔐 Auth interceptor - Token set in headers');
|
||||
} else {
|
||||
console.warn('⚠️ Auth interceptor - No token available');
|
||||
}
|
||||
return config;
|
||||
});
|
||||
@@ -67,6 +71,10 @@ export interface Document {
|
||||
analysis_data?: any; // BPCP CIM Review Template data
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
// GCS-specific fields
|
||||
gcs_path?: string;
|
||||
gcs_url?: string;
|
||||
storage_type?: 'gcs' | 'local';
|
||||
}
|
||||
|
||||
export interface UploadProgress {
|
||||
@@ -148,6 +156,67 @@ export interface CIMReviewData {
|
||||
};
|
||||
}
|
||||
|
||||
// GCS-specific error types
|
||||
export interface GCSError {
|
||||
type: 'gcs_upload_error' | 'gcs_download_error' | 'gcs_permission_error' | 'gcs_quota_error' | 'gcs_network_error';
|
||||
message: string;
|
||||
details?: any;
|
||||
retryable: boolean;
|
||||
}
|
||||
|
||||
// Enhanced error handling for GCS operations
|
||||
export class GCSErrorHandler {
|
||||
static isGCSError(error: any): error is GCSError {
|
||||
return error && typeof error === 'object' && 'type' in error && error.type?.startsWith('gcs_');
|
||||
}
|
||||
|
||||
static createGCSError(error: any, operation: string): GCSError {
|
||||
const errorMessage = error?.message || error?.toString() || 'Unknown GCS error';
|
||||
|
||||
// Determine error type based on error message or response
|
||||
let type: GCSError['type'] = 'gcs_network_error';
|
||||
let retryable = true;
|
||||
|
||||
if (errorMessage.includes('permission') || errorMessage.includes('access denied')) {
|
||||
type = 'gcs_permission_error';
|
||||
retryable = false;
|
||||
} else if (errorMessage.includes('quota') || errorMessage.includes('storage quota')) {
|
||||
type = 'gcs_quota_error';
|
||||
retryable = false;
|
||||
} else if (errorMessage.includes('upload') || errorMessage.includes('write')) {
|
||||
type = 'gcs_upload_error';
|
||||
retryable = true;
|
||||
} else if (errorMessage.includes('download') || errorMessage.includes('read')) {
|
||||
type = 'gcs_download_error';
|
||||
retryable = true;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
message: `${operation} failed: ${errorMessage}`,
|
||||
details: error,
|
||||
retryable
|
||||
};
|
||||
}
|
||||
|
||||
static getErrorMessage(error: GCSError): string {
|
||||
switch (error.type) {
|
||||
case 'gcs_permission_error':
|
||||
return 'Access denied. Please check your permissions and try again.';
|
||||
case 'gcs_quota_error':
|
||||
return 'Storage quota exceeded. Please contact support.';
|
||||
case 'gcs_upload_error':
|
||||
return 'Upload failed. Please check your connection and try again.';
|
||||
case 'gcs_download_error':
|
||||
return 'Download failed. Please try again later.';
|
||||
case 'gcs_network_error':
|
||||
return 'Network error. Please check your connection and try again.';
|
||||
default:
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentService {
|
||||
/**
|
||||
* Upload a document for processing
|
||||
@@ -157,33 +226,89 @@ class DocumentService {
|
||||
onProgress?: (progress: number) => void,
|
||||
signal?: AbortSignal
|
||||
): Promise<Document> {
|
||||
const formData = new FormData();
|
||||
formData.append('document', file);
|
||||
|
||||
// Always use optimized agentic RAG processing - no strategy selection needed
|
||||
formData.append('processingStrategy', 'optimized_agentic_rag');
|
||||
try {
|
||||
// Check authentication before upload
|
||||
const token = await authService.getToken();
|
||||
if (!token) {
|
||||
throw new Error('Authentication required. Please log in to upload documents.');
|
||||
}
|
||||
|
||||
const response = await apiClient.post('/api/documents', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
signal, // Add abort signal support
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (onProgress && progressEvent.total) {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
onProgress(progress);
|
||||
console.log('📤 Starting document upload...');
|
||||
console.log('📤 File:', file.name, 'Size:', file.size, 'Type:', file.type);
|
||||
console.log('📤 Token available:', !!token);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('document', file);
|
||||
|
||||
// Always use optimized agentic RAG processing - no strategy selection needed
|
||||
formData.append('processingStrategy', 'optimized_agentic_rag');
|
||||
|
||||
const response = await apiClient.post('/documents', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
signal, // Add abort signal support
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (onProgress && progressEvent.total) {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
onProgress(progress);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ Document upload successful:', response.data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('❌ Document upload failed:', error);
|
||||
|
||||
// Provide more specific error messages
|
||||
if (error.response?.status === 401) {
|
||||
if (error.response?.data?.error === 'No valid authorization header') {
|
||||
throw new Error('Authentication required. Please log in to upload documents.');
|
||||
} else if (error.response?.data?.error === 'Token expired') {
|
||||
throw new Error('Your session has expired. Please log in again.');
|
||||
} else if (error.response?.data?.error === 'Invalid token') {
|
||||
throw new Error('Authentication failed. Please log in again.');
|
||||
} else {
|
||||
throw new Error('Authentication error. Please log in again.');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} else if (error.response?.status === 400) {
|
||||
if (error.response?.data?.error === 'No file uploaded') {
|
||||
throw new Error('No file was selected for upload.');
|
||||
} else if (error.response?.data?.error === 'File too large') {
|
||||
throw new Error('File is too large. Please select a smaller file.');
|
||||
} else if (error.response?.data?.error === 'File type not allowed') {
|
||||
throw new Error('File type not supported. Please upload a PDF or text file.');
|
||||
} else {
|
||||
throw new Error(`Upload failed: ${error.response?.data?.error || 'Bad request'}`);
|
||||
}
|
||||
} else if (error.response?.status === 413) {
|
||||
throw new Error('File is too large. Please select a smaller file.');
|
||||
} else if (error.response?.status >= 500) {
|
||||
throw new Error('Server error. Please try again later.');
|
||||
} else if (error.code === 'ERR_NETWORK') {
|
||||
throw new Error('Network error. Please check your connection and try again.');
|
||||
} else if (error.name === 'AbortError') {
|
||||
throw new Error('Upload was cancelled.');
|
||||
}
|
||||
|
||||
// Handle GCS-specific errors
|
||||
if (error.response?.data?.type === 'storage_error' ||
|
||||
error.message?.includes('GCS') ||
|
||||
error.message?.includes('storage.googleapis.com')) {
|
||||
throw GCSErrorHandler.createGCSError(error, 'upload');
|
||||
}
|
||||
|
||||
// Generic error fallback
|
||||
throw new Error(error.response?.data?.error || error.message || 'Upload failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all documents for the current user
|
||||
*/
|
||||
async getDocuments(): Promise<Document[]> {
|
||||
const response = await apiClient.get('/api/documents');
|
||||
const response = await apiClient.get('/documents');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -191,7 +316,7 @@ class DocumentService {
|
||||
* Get a specific document by ID
|
||||
*/
|
||||
async getDocument(documentId: string): Promise<Document> {
|
||||
const response = await apiClient.get(`/api/documents/${documentId}`);
|
||||
const response = await apiClient.get(`/documents/${documentId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -199,7 +324,7 @@ class DocumentService {
|
||||
* Get document processing status
|
||||
*/
|
||||
async getDocumentStatus(documentId: string): Promise<{ status: string; progress: number; message?: string }> {
|
||||
const response = await apiClient.get(`/api/documents/${documentId}/progress`);
|
||||
const response = await apiClient.get(`/documents/${documentId}/progress`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -207,24 +332,34 @@ class DocumentService {
|
||||
* Download a processed document
|
||||
*/
|
||||
async downloadDocument(documentId: string): Promise<Blob> {
|
||||
const response = await apiClient.get(`/api/documents/${documentId}/download`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
try {
|
||||
const response = await apiClient.get(`/documents/${documentId}/download`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
// Handle GCS-specific errors
|
||||
if (error.response?.data?.type === 'storage_error' ||
|
||||
error.message?.includes('GCS') ||
|
||||
error.message?.includes('storage.googleapis.com')) {
|
||||
throw GCSErrorHandler.createGCSError(error, 'download');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document
|
||||
*/
|
||||
async deleteDocument(documentId: string): Promise<void> {
|
||||
await apiClient.delete(`/api/documents/${documentId}`);
|
||||
await apiClient.delete(`/documents/${documentId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry processing for a failed document
|
||||
*/
|
||||
async retryProcessing(documentId: string): Promise<Document> {
|
||||
const response = await apiClient.post(`/api/documents/${documentId}/retry`);
|
||||
const response = await apiClient.post(`/documents/${documentId}/retry`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -232,14 +367,14 @@ class DocumentService {
|
||||
* Save CIM review data
|
||||
*/
|
||||
async saveCIMReview(documentId: string, reviewData: CIMReviewData): Promise<void> {
|
||||
await apiClient.post(`/api/documents/${documentId}/review`, reviewData);
|
||||
await apiClient.post(`/documents/${documentId}/review`, reviewData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CIM review data for a document
|
||||
*/
|
||||
async getCIMReview(documentId: string): Promise<CIMReviewData> {
|
||||
const response = await apiClient.get(`/api/documents/${documentId}/review`);
|
||||
const response = await apiClient.get(`/documents/${documentId}/review`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -247,7 +382,7 @@ class DocumentService {
|
||||
* Export CIM review as PDF
|
||||
*/
|
||||
async exportCIMReview(documentId: string): Promise<Blob> {
|
||||
const response = await apiClient.get(`/api/documents/${documentId}/export`, {
|
||||
const response = await apiClient.get(`/documents/${documentId}/export`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
@@ -257,7 +392,7 @@ class DocumentService {
|
||||
* Get document analytics and insights
|
||||
*/
|
||||
async getDocumentAnalytics(documentId: string): Promise<any> {
|
||||
const response = await apiClient.get(`/api/documents/${documentId}/analytics`);
|
||||
const response = await apiClient.get(`/documents/${documentId}/analytics`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -265,7 +400,7 @@ class DocumentService {
|
||||
* Get global analytics data
|
||||
*/
|
||||
async getAnalytics(days: number = 30): Promise<any> {
|
||||
const response = await apiClient.get('/api/documents/analytics', {
|
||||
const response = await apiClient.get('/documents/analytics', {
|
||||
params: { days }
|
||||
});
|
||||
return response.data;
|
||||
@@ -275,7 +410,7 @@ class DocumentService {
|
||||
* Get processing statistics
|
||||
*/
|
||||
async getProcessingStats(): Promise<any> {
|
||||
const response = await apiClient.get('/api/documents/processing-stats');
|
||||
const response = await apiClient.get('/documents/processing-stats');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -283,7 +418,7 @@ class DocumentService {
|
||||
* Get agentic RAG sessions for a document
|
||||
*/
|
||||
async getAgenticRAGSessions(documentId: string): Promise<any> {
|
||||
const response = await apiClient.get(`/api/documents/${documentId}/agentic-rag-sessions`);
|
||||
const response = await apiClient.get(`/documents/${documentId}/agentic-rag-sessions`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -291,7 +426,7 @@ class DocumentService {
|
||||
* Get detailed agentic RAG session information
|
||||
*/
|
||||
async getAgenticRAGSessionDetails(sessionId: string): Promise<any> {
|
||||
const response = await apiClient.get(`/api/documents/agentic-rag-sessions/${sessionId}`);
|
||||
const response = await apiClient.get(`/documents/agentic-rag-sessions/${sessionId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -315,7 +450,7 @@ class DocumentService {
|
||||
* Search documents
|
||||
*/
|
||||
async searchDocuments(query: string): Promise<Document[]> {
|
||||
const response = await apiClient.get('/api/documents/search', {
|
||||
const response = await apiClient.get('/documents/search', {
|
||||
params: { q: query },
|
||||
});
|
||||
return response.data;
|
||||
@@ -325,7 +460,7 @@ class DocumentService {
|
||||
* Get processing queue status
|
||||
*/
|
||||
async getQueueStatus(): Promise<{ pending: number; processing: number; completed: number; failed: number }> {
|
||||
const response = await apiClient.get('/api/documents/queue/status');
|
||||
const response = await apiClient.get('/documents/queue/status');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -376,11 +511,36 @@ class DocumentService {
|
||||
|
||||
/**
|
||||
* Generate a download URL for a document
|
||||
* Handles both GCS direct URLs and API proxy URLs
|
||||
*/
|
||||
getDownloadUrl(documentId: string): string {
|
||||
getDownloadUrl(documentId: string, document?: Document): string {
|
||||
// If document has a GCS URL, use it directly for better performance
|
||||
if (document?.gcs_url && document.storage_type === 'gcs') {
|
||||
return document.gcs_url;
|
||||
}
|
||||
|
||||
// Fallback to API proxy URL
|
||||
return `${API_BASE_URL}/documents/${documentId}/download`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a document is stored in GCS
|
||||
*/
|
||||
isGCSDocument(document: Document): boolean {
|
||||
return document.storage_type === 'gcs' || !!document.gcs_path || !!document.gcs_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GCS-specific file info
|
||||
*/
|
||||
getGCSFileInfo(document: Document): { gcsPath?: string; gcsUrl?: string; storageType: string } {
|
||||
return {
|
||||
gcsPath: document.gcs_path,
|
||||
gcsUrl: document.gcs_url,
|
||||
storageType: document.storage_type || 'unknown'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size for display
|
||||
*/
|
||||
|
||||
110
frontend/src/utils/authDebug.ts
Normal file
110
frontend/src/utils/authDebug.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { authService } from '../services/authService';
|
||||
|
||||
export const debugAuth = async () => {
|
||||
console.log('🔍 Debugging authentication...');
|
||||
|
||||
try {
|
||||
// Check if user is authenticated
|
||||
const isAuthenticated = authService.isAuthenticated();
|
||||
console.log('🔍 Is authenticated:', isAuthenticated);
|
||||
|
||||
if (isAuthenticated) {
|
||||
// Get current user
|
||||
const user = authService.getCurrentUser();
|
||||
console.log('🔍 Current user:', user);
|
||||
|
||||
// Get token
|
||||
const token = await authService.getToken();
|
||||
console.log('🔍 Token available:', !!token);
|
||||
console.log('🔍 Token length:', token?.length);
|
||||
console.log('🔍 Token preview:', token ? `${token.substring(0, 20)}...` : 'No token');
|
||||
|
||||
// Test token format
|
||||
if (token) {
|
||||
const parts = token.split('.');
|
||||
console.log('🔍 Token parts:', parts.length);
|
||||
if (parts.length === 3) {
|
||||
try {
|
||||
const header = JSON.parse(atob(parts[0]));
|
||||
const payload = JSON.parse(atob(parts[1]));
|
||||
console.log('🔍 Token header:', header);
|
||||
console.log('🔍 Token payload:', {
|
||||
iss: payload.iss,
|
||||
aud: payload.aud,
|
||||
auth_time: payload.auth_time,
|
||||
exp: payload.exp,
|
||||
iat: payload.iat,
|
||||
user_id: payload.user_id,
|
||||
email: payload.email
|
||||
});
|
||||
|
||||
// Check if token is expired
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const isExpired = payload.exp && payload.exp < now;
|
||||
console.log('🔍 Token expired:', isExpired);
|
||||
console.log('🔍 Current time:', now);
|
||||
console.log('🔍 Token expires:', payload.exp);
|
||||
} catch (error) {
|
||||
console.error('🔍 Error parsing token:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('🔍 User is not authenticated');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('🔍 Auth debug error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Export a function to test API authentication
|
||||
export const testAPIAuth = async () => {
|
||||
console.log('🧪 Testing API authentication...');
|
||||
|
||||
try {
|
||||
const token = await authService.getToken();
|
||||
if (!token) {
|
||||
console.log('❌ No token available');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('🔐 Token available, length:', token.length);
|
||||
console.log('🔐 Token preview:', token.substring(0, 20) + '...');
|
||||
console.log('🔐 Token format check:', token.split('.').length === 3 ? 'Valid JWT format' : 'Invalid format');
|
||||
|
||||
// Test the API endpoint
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/documents`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('🧪 API response status:', response.status);
|
||||
console.log('🧪 API response headers:', Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('✅ API authentication successful');
|
||||
console.log('🧪 Response data:', data);
|
||||
return true;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.log('❌ API authentication failed');
|
||||
console.log('🧪 Error response:', errorText);
|
||||
|
||||
// Try to parse error as JSON
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText);
|
||||
console.log('❌ Error details:', errorJson);
|
||||
} catch {
|
||||
console.log('❌ Raw error:', errorText);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ API test error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user