🔧 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:
Jon
2025-07-31 16:18:53 -04:00
parent aa0931ecd7
commit 6057d1d7fd
79 changed files with 8920 additions and 1786 deletions

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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",

View File

@@ -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>

View File

@@ -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 */}

View File

@@ -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>

View File

@@ -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',

View File

@@ -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',

View 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 (&lt; 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 (&gt; 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 (&lt; 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 (&gt; 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;

View File

@@ -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(','),

View File

@@ -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
*/

View 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;
}
};