feat: 7 Vorbereitungs-Module auf 100% — Frontend, Proxy-Routen, Backend-Fixes
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
Profil: machineBuilder-Felder im POST-Body, PATCH-Handler Scope: API-Route (GET/POST), ScopeDecisionTab Props + Buttons, Export-Druckansicht HTML Anwendung: PUT-Handler, Bearbeiten-Button, Pagination/Search Import: Verlauf laden, DELETE-Route, Offline-Badge, ObjectURL Memory-Leak fix Screening: Security-Backlog Button verdrahtet, Scan-Verlauf Module: Detail-Seite, GET-Proxy, Konfigurieren-Button, Modul-erstellen-Modal, Error-Toast Quellen: 10 Proxy-Routen, Tab-Komponenten umgestellt, Dashboard-Tab, blocked_today Bug fix, Datum-Filter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
import type { ImportedDocument, ImportedDocumentType, GapAnalysis, GapItem } from '@/lib/sdk/types'
|
||||
@@ -216,7 +216,15 @@ function FileItem({
|
||||
<span className="text-sm">Analysiere...</span>
|
||||
</div>
|
||||
)}
|
||||
{file.status === 'complete' && (
|
||||
{file.status === 'complete' && file.error === 'offline' && (
|
||||
<div className="flex items-center gap-1 text-amber-600">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span className="text-sm">Offline — nicht analysiert</span>
|
||||
</div>
|
||||
)}
|
||||
{file.status === 'complete' && file.error !== 'offline' && (
|
||||
<div className="flex items-center gap-1 text-green-600">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
@@ -334,6 +342,42 @@ export default function ImportPage() {
|
||||
const [files, setFiles] = useState<UploadedFile[]>([])
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||
const [analysisResult, setAnalysisResult] = useState<GapAnalysis | null>(null)
|
||||
const [importHistory, setImportHistory] = useState<any[]>([])
|
||||
const [historyLoading, setHistoryLoading] = useState(false)
|
||||
const [objectUrls, setObjectUrls] = useState<string[]>([])
|
||||
|
||||
// 4.1: Load import history
|
||||
useEffect(() => {
|
||||
const loadHistory = async () => {
|
||||
setHistoryLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/sdk/v1/import?tenant_id=default')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setImportHistory(Array.isArray(data) ? data : data.items || [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load import history:', err)
|
||||
} finally {
|
||||
setHistoryLoading(false)
|
||||
}
|
||||
}
|
||||
loadHistory()
|
||||
}, [analysisResult])
|
||||
|
||||
// 4.4: Cleanup ObjectURLs on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
objectUrls.forEach(url => URL.revokeObjectURL(url))
|
||||
}
|
||||
}, [objectUrls])
|
||||
|
||||
// Helper to create and track ObjectURLs
|
||||
const createTrackedObjectURL = useCallback((file: File) => {
|
||||
const url = URL.createObjectURL(file)
|
||||
setObjectUrls(prev => [...prev, url])
|
||||
return url
|
||||
}, [])
|
||||
|
||||
const handleFilesAdded = useCallback((newFiles: File[]) => {
|
||||
const uploadedFiles: UploadedFile[] = newFiles.map(file => ({
|
||||
@@ -391,7 +435,7 @@ export default function ImportPage() {
|
||||
id: result.document_id || file.id,
|
||||
name: file.file.name,
|
||||
type: result.detected_type || file.type,
|
||||
fileUrl: URL.createObjectURL(file.file),
|
||||
fileUrl: createTrackedObjectURL(file.file),
|
||||
uploadedAt: new Date(),
|
||||
analyzedAt: new Date(),
|
||||
analysisResult: {
|
||||
@@ -430,7 +474,7 @@ export default function ImportPage() {
|
||||
id: file.id,
|
||||
name: file.file.name,
|
||||
type: file.type,
|
||||
fileUrl: URL.createObjectURL(file.file),
|
||||
fileUrl: createTrackedObjectURL(file.file),
|
||||
uploadedAt: new Date(),
|
||||
analyzedAt: new Date(),
|
||||
analysisResult: {
|
||||
@@ -442,7 +486,7 @@ export default function ImportPage() {
|
||||
},
|
||||
}
|
||||
addImportedDocument(doc)
|
||||
setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, progress: 100, status: 'complete' as const } : f)))
|
||||
setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, progress: 100, status: 'complete' as const, error: 'offline' } : f)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +609,56 @@ export default function ImportPage() {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Import-Verlauf (4.1) */}
|
||||
{importHistory.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 className="font-semibold text-gray-900">Import-Verlauf</h3>
|
||||
<p className="text-sm text-gray-500">{importHistory.length} fruehere Imports</p>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{importHistory.map((item: any, idx: number) => (
|
||||
<div key={item.id || idx} className="px-6 py-4 flex items-center justify-between hover:bg-gray-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">{item.name || item.filename || `Import #${idx + 1}`}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{item.document_type || item.type || 'Unbekannt'} — {item.uploaded_at ? new Date(item.uploaded_at).toLocaleString('de-DE') : 'Unbekannt'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/import/${item.id}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
setImportHistory(prev => prev.filter(h => h.id !== item.id))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete import:', err)
|
||||
}
|
||||
}}
|
||||
className="p-2 text-gray-400 hover:text-red-500 transition-colors"
|
||||
title="Import loeschen"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{historyLoading && (
|
||||
<div className="text-center py-4 text-sm text-gray-500">Import-Verlauf wird geladen...</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user