diff --git a/admin-compliance/app/sdk/screening/page.tsx b/admin-compliance/app/sdk/screening/page.tsx
index 8f58fce..99e58da 100644
--- a/admin-compliance/app/sdk/screening/page.tsx
+++ b/admin-compliance/app/sdk/screening/page.tsx
@@ -8,38 +8,18 @@ import { useSDK, ScreeningResult, SecurityIssue, SBOMComponent, BacklogItem } fr
// COMPONENTS
// =============================================================================
-function ScanProgress({ progress, status }: { progress: number; status: string }) {
+function ScanProgress({ status }: { status: string }) {
return (
-
-
-
- {progress}%
-
-
+
-
Scanning...
+
Scan läuft...
{status}
-
)
@@ -166,7 +146,6 @@ export default function ScreeningPage() {
const { state, dispatch } = useSDK()
const router = useRouter()
const [isScanning, setIsScanning] = useState(false)
- const [scanProgress, setScanProgress] = useState(0)
const [scanStatus, setScanStatus] = useState('')
const [scanError, setScanError] = useState
(null)
const [scanHistory, setScanHistory] = useState([])
@@ -214,27 +193,22 @@ export default function ScreeningPage() {
const startScan = async (file: File) => {
setIsScanning(true)
- setScanProgress(0)
- setScanStatus('Initialisierung...')
+ setScanStatus('Abhängigkeiten werden analysiert...')
setScanError(null)
- // Show progress steps while API processes
- const progressInterval = setInterval(() => {
- setScanProgress(prev => {
- if (prev >= 90) return prev
- const step = Math.random() * 15 + 5
- const next = Math.min(prev + step, 90)
- const statuses = [
- 'Abhaengigkeiten werden analysiert...',
- 'SBOM wird generiert...',
- 'Schwachstellenscan laeuft...',
- 'OSV.dev Datenbank wird abgefragt...',
- 'Lizenzpruefung...',
- ]
- setScanStatus(statuses[Math.min(Math.floor(next / 20), statuses.length - 1)])
- return next
- })
- }, 600)
+ // Rotate through honest status messages while backend processes
+ const statusMessages = [
+ 'Abhängigkeiten werden analysiert...',
+ 'SBOM wird generiert...',
+ 'Schwachstellenscan läuft...',
+ 'OSV.dev Datenbank wird abgefragt...',
+ 'Lizenzprüfung...',
+ ]
+ let statusIdx = 0
+ const statusInterval = setInterval(() => {
+ statusIdx = (statusIdx + 1) % statusMessages.length
+ setScanStatus(statusMessages[statusIdx])
+ }, 2000)
try {
const formData = new FormData()
@@ -246,7 +220,7 @@ export default function ScreeningPage() {
body: formData,
})
- clearInterval(progressInterval)
+ clearInterval(statusInterval)
if (!response.ok) {
const err = await response.json().catch(() => ({ error: 'Unknown error' }))
@@ -255,7 +229,6 @@ export default function ScreeningPage() {
const data = await response.json()
- setScanProgress(100)
setScanStatus('Abgeschlossen!')
// Map backend response to ScreeningResult
@@ -308,10 +281,9 @@ export default function ScreeningPage() {
dispatch({ type: 'ADD_SECURITY_ISSUE', payload: issue })
})
} catch (error: any) {
- clearInterval(progressInterval)
+ clearInterval(statusInterval)
console.error('Screening scan failed:', error)
setScanError(error.message || 'Scan fehlgeschlagen')
- setScanProgress(0)
setScanStatus('')
} finally {
setIsScanning(false)
@@ -369,7 +341,7 @@ export default function ScreeningPage() {
)}
{/* Scan Progress */}
- {isScanning && }
+ {isScanning && }
{/* Results */}
{state.screening && state.screening.status === 'COMPLETED' && (
diff --git a/backend-compliance/compliance/api/import_routes.py b/backend-compliance/compliance/api/import_routes.py
index f0199c9..b280bf3 100644
--- a/backend-compliance/compliance/api/import_routes.py
+++ b/backend-compliance/compliance/api/import_routes.py
@@ -400,3 +400,46 @@ async def list_documents(tenant_id: str = "default"):
return DocumentListResponse(documents=documents, total=len(documents))
finally:
db.close()
+
+
+@router.get("", response_model=DocumentListResponse)
+async def list_documents_root(
+ tenant_id: str = "default",
+ x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
+):
+ """Alias: GET /v1/import → list documents (proxy-compatible URL)."""
+ tid = x_tenant_id or tenant_id
+ return await list_documents(tenant_id=tid)
+
+
+@router.delete("/{document_id}")
+async def delete_document(
+ document_id: str,
+ tenant_id: str = "default",
+ x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
+):
+ """Delete an imported document and its gap analysis."""
+ tid = x_tenant_id or tenant_id
+ db = SessionLocal()
+ try:
+ # Delete gap analysis first (FK dependency)
+ db.execute(
+ "DELETE FROM compliance_gap_analyses WHERE document_id = :doc_id AND tenant_id = :tid",
+ {"doc_id": document_id, "tid": tid},
+ )
+ result = db.execute(
+ "DELETE FROM compliance_imported_documents WHERE id = :doc_id AND tenant_id = :tid",
+ {"doc_id": document_id, "tid": tid},
+ )
+ db.commit()
+ if result.rowcount == 0:
+ raise HTTPException(status_code=404, detail="Document not found")
+ return {"success": True, "deleted_id": document_id}
+ except HTTPException:
+ raise
+ except Exception as e:
+ db.rollback()
+ logger.error(f"Failed to delete document {document_id}: {e}")
+ raise HTTPException(status_code=500, detail="Failed to delete document")
+ finally:
+ db.close()
diff --git a/backend-compliance/tests/test_import_routes.py b/backend-compliance/tests/test_import_routes.py
index 0c80c92..997fb7b 100644
--- a/backend-compliance/tests/test_import_routes.py
+++ b/backend-compliance/tests/test_import_routes.py
@@ -557,3 +557,113 @@ class TestGapAnalysisEndpoint:
# execute call should use "header-tenant" (X-Tenant-ID takes precedence)
call_args = mock_session.execute.call_args
assert "header-tenant" in str(call_args)
+
+
+class TestListDocumentsRootEndpoint:
+ """API tests for GET /v1/import (root alias — proxy-compatible URL)."""
+
+ def test_root_alias_returns_documents(self):
+ """GET /v1/import returns same result as /v1/import/documents."""
+ with patch("compliance.api.import_routes.SessionLocal") as MockSL:
+ mock_session = MagicMock()
+ MockSL.return_value = mock_session
+ mock_result = MagicMock()
+ mock_result.fetchall.return_value = [
+ ["uuid-1", "dsfa.pdf", "application/pdf", 1024, "DSFA", 0.85,
+ [], [], "analyzed", None, "2024-01-15"],
+ ]
+ mock_session.execute.return_value = mock_result
+
+ response = _client_import.get("/v1/import", params={"tenant_id": TENANT_ID})
+
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total"] == 1
+ assert data["documents"][0]["filename"] == "dsfa.pdf"
+
+ def test_root_alias_empty(self):
+ """GET /v1/import returns empty list when no documents."""
+ with patch("compliance.api.import_routes.SessionLocal") as MockSL:
+ mock_session = MagicMock()
+ MockSL.return_value = mock_session
+ mock_result = MagicMock()
+ mock_result.fetchall.return_value = []
+ mock_session.execute.return_value = mock_result
+
+ response = _client_import.get("/v1/import", params={"tenant_id": TENANT_ID})
+
+ assert response.status_code == 200
+ assert response.json()["total"] == 0
+
+
+class TestDeleteDocumentEndpoint:
+ """API tests for DELETE /v1/import/{document_id}."""
+
+ def test_delete_existing_document(self):
+ """Deletes document and returns success."""
+ with patch("compliance.api.import_routes.SessionLocal") as MockSL:
+ mock_session = MagicMock()
+ MockSL.return_value = mock_session
+ mock_result = MagicMock()
+ mock_result.rowcount = 1
+ mock_session.execute.return_value = mock_result
+
+ response = _client_import.delete(
+ "/v1/import/doc-uuid-001",
+ params={"tenant_id": TENANT_ID},
+ )
+
+ assert response.status_code == 200
+ data = response.json()
+ assert data["success"] is True
+ assert data["deleted_id"] == "doc-uuid-001"
+ mock_session.commit.assert_called_once()
+
+ def test_delete_not_found(self):
+ """Returns 404 when document does not exist for tenant."""
+ with patch("compliance.api.import_routes.SessionLocal") as MockSL:
+ mock_session = MagicMock()
+ MockSL.return_value = mock_session
+ mock_result = MagicMock()
+ mock_result.rowcount = 0
+ mock_session.execute.return_value = mock_result
+
+ response = _client_import.delete(
+ "/v1/import/nonexistent-doc",
+ params={"tenant_id": TENANT_ID},
+ )
+
+ assert response.status_code == 404
+
+ def test_delete_uses_tenant_isolation(self):
+ """Tenant ID is passed to the delete query."""
+ with patch("compliance.api.import_routes.SessionLocal") as MockSL:
+ mock_session = MagicMock()
+ MockSL.return_value = mock_session
+ mock_result = MagicMock()
+ mock_result.rowcount = 1
+ mock_session.execute.return_value = mock_result
+
+ _client_import.delete(
+ "/v1/import/doc-uuid",
+ params={"tenant_id": "custom-tenant"},
+ )
+
+ # Both execute calls should use the tenant ID
+ call_args_list = mock_session.execute.call_args_list
+ for call in call_args_list:
+ assert "custom-tenant" in str(call)
+
+ def test_delete_db_error_returns_500(self):
+ """Database error returns 500."""
+ with patch("compliance.api.import_routes.SessionLocal") as MockSL:
+ mock_session = MagicMock()
+ MockSL.return_value = mock_session
+ mock_session.execute.side_effect = Exception("DB error")
+
+ response = _client_import.delete(
+ "/v1/import/doc-uuid",
+ params={"tenant_id": TENANT_ID},
+ )
+
+ assert response.status_code == 500