diff --git a/admin-compliance/app/api/sdk/v1/projects/[projectId]/permanent/route.ts b/admin-compliance/app/api/sdk/v1/projects/[projectId]/permanent/route.ts
new file mode 100644
index 0000000..089bb7b
--- /dev/null
+++ b/admin-compliance/app/api/sdk/v1/projects/[projectId]/permanent/route.ts
@@ -0,0 +1,41 @@
+import { NextRequest, NextResponse } from 'next/server'
+
+const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
+
+/**
+ * Proxy: DELETE /api/sdk/v1/projects/{projectId}/permanent → Backend (hard delete)
+ */
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ try {
+ const { projectId } = await params
+ const tenantId = request.headers.get('X-Tenant-ID') ||
+ new URL(request.url).searchParams.get('tenant_id') || ''
+
+ const response = await fetch(
+ `${BACKEND_URL}/api/compliance/v1/projects/${projectId}/permanent?tenant_id=${encodeURIComponent(tenantId)}`,
+ {
+ method: 'DELETE',
+ headers: { 'X-Tenant-ID': tenantId },
+ }
+ )
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ return NextResponse.json(
+ { error: 'Backend error', details: errorText },
+ { status: response.status }
+ )
+ }
+
+ return NextResponse.json(await response.json())
+ } catch (error) {
+ console.error('Failed to permanently delete project:', error)
+ return NextResponse.json(
+ { error: 'Failed to connect to backend' },
+ { status: 503 }
+ )
+ }
+}
diff --git a/admin-compliance/app/api/sdk/v1/projects/[projectId]/restore/route.ts b/admin-compliance/app/api/sdk/v1/projects/[projectId]/restore/route.ts
new file mode 100644
index 0000000..a4c93bf
--- /dev/null
+++ b/admin-compliance/app/api/sdk/v1/projects/[projectId]/restore/route.ts
@@ -0,0 +1,41 @@
+import { NextRequest, NextResponse } from 'next/server'
+
+const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
+
+/**
+ * Proxy: POST /api/sdk/v1/projects/{projectId}/restore → Backend
+ */
+export async function POST(
+ request: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ try {
+ const { projectId } = await params
+ const tenantId = request.headers.get('X-Tenant-ID') ||
+ new URL(request.url).searchParams.get('tenant_id') || ''
+
+ const response = await fetch(
+ `${BACKEND_URL}/api/compliance/v1/projects/${projectId}/restore?tenant_id=${encodeURIComponent(tenantId)}`,
+ {
+ method: 'POST',
+ headers: { 'X-Tenant-ID': tenantId },
+ }
+ )
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ return NextResponse.json(
+ { error: 'Backend error', details: errorText },
+ { status: response.status }
+ )
+ }
+
+ return NextResponse.json(await response.json())
+ } catch (error) {
+ console.error('Failed to restore project:', error)
+ return NextResponse.json(
+ { error: 'Failed to connect to backend' },
+ { status: 503 }
+ )
+ }
+}
diff --git a/admin-compliance/app/sdk/page.tsx b/admin-compliance/app/sdk/page.tsx
index eefc5ac..f252461 100644
--- a/admin-compliance/app/sdk/page.tsx
+++ b/admin-compliance/app/sdk/page.tsx
@@ -3,9 +3,8 @@
import React from 'react'
import Link from 'next/link'
import { useSDK, SDK_PACKAGES, getStepsForPackage } from '@/lib/sdk'
-import { CustomerTypeSelector } from '@/components/sdk/CustomerTypeSelector'
import { ProjectSelector } from '@/components/sdk/ProjectSelector/ProjectSelector'
-import type { CustomerType, SDKPackageId } from '@/lib/sdk/types'
+import type { SDKPackageId } from '@/lib/sdk/types'
// =============================================================================
// DASHBOARD CARDS
@@ -171,6 +170,9 @@ function QuickActionCard({
export default function SDKDashboard() {
const { state, packageCompletion, completionPercentage, setCustomerType, projectId } = useSDK()
+ // customerType is set during project creation — default to 'new' for legacy projects
+ const effectiveCustomerType = state.customerType || 'new'
+
// No project selected → show project list
if (!projectId) {
return
@@ -180,7 +182,7 @@ export default function SDKDashboard() {
const totalSteps = SDK_PACKAGES.reduce((sum, pkg) => {
const steps = getStepsForPackage(pkg.id)
// Filter import step for new customers
- return sum + steps.filter(s => !(s.id === 'import' && state.customerType === 'new')).length
+ return sum + steps.filter(s => !(s.id === 'import' && effectiveCustomerType === 'new')).length
}, 0)
// Calculate stats
@@ -200,19 +202,6 @@ export default function SDKDashboard() {
return packageCompletion[prevPkg.id] < 100
}
- // Show customer type selector if not set
- if (!state.customerType) {
- return (
-
- {
- setCustomerType(type)
- }}
- />
-
- )
- }
-
return (
{/* Header */}
@@ -220,16 +209,16 @@ export default function SDKDashboard() {
AI Compliance SDK
- {state.customerType === 'new'
+ {effectiveCustomerType === 'new'
? 'Neukunden-Modus: Erstellen Sie alle Compliance-Dokumente von Grund auf.'
: 'Bestandskunden-Modus: Erweitern Sie bestehende Dokumente.'}
setCustomerType(state.customerType === 'new' ? 'existing' : 'new')}
+ onClick={() => setCustomerType(effectiveCustomerType === 'new' ? 'existing' : 'new')}
className="text-sm text-purple-600 hover:text-purple-700 underline"
>
- {state.customerType === 'new' ? 'Zu Bestandskunden wechseln' : 'Zu Neukunden wechseln'}
+ {effectiveCustomerType === 'new' ? 'Zu Bestandskunden wechseln' : 'Zu Neukunden wechseln'}
@@ -282,7 +271,7 @@ export default function SDKDashboard() {
{/* Bestandskunden: Gap Analysis Banner */}
- {state.customerType === 'existing' && state.importedDocuments.length === 0 && (
+ {effectiveCustomerType === 'existing' && state.importedDocuments.length === 0 && (
@@ -348,7 +337,7 @@ export default function SDKDashboard() {
{SDK_PACKAGES.map(pkg => {
const steps = getStepsForPackage(pkg.id)
- const visibleSteps = steps.filter(s => !(s.id === 'import' && state.customerType === 'new'))
+ const visibleSteps = steps.filter(s => !(s.id === 'import' && effectiveCustomerType === 'new'))
return (
1 ? 'en' : ''}`
+}
+
// =============================================================================
// CREATE PROJECT DIALOG
// =============================================================================
@@ -41,7 +76,7 @@ function CreateProjectDialog({ open, onClose, onCreated, existingProjects }: Cre
customerType,
copyFromId || undefined
)
- onCreated(project)
+ onCreated(normalizeProject(project))
setName('')
setCopyFromId('')
onClose()
@@ -164,73 +199,253 @@ function CreateProjectDialog({ open, onClose, onCreated, existingProjects }: Cre
}
// =============================================================================
-// PROJECT CARD
+// PROJECT ACTION DIALOG (Archive / Permanent Delete)
// =============================================================================
-function ProjectCard({ project, onClick }: { project: ProjectInfo; onClick: () => void }) {
- const timeAgo = formatTimeAgo(project.updatedAt)
+type ActionStep = 'choose' | 'confirm-delete'
+
+function ProjectActionDialog({
+ project,
+ onArchive,
+ onPermanentDelete,
+ onCancel,
+ isProcessing,
+}: {
+ project: ProjectInfo
+ onArchive: () => void
+ onPermanentDelete: () => void
+ onCancel: () => void
+ isProcessing: boolean
+}) {
+ const [step, setStep] = useState('choose')
+
+ if (step === 'confirm-delete') {
+ return (
+
+
e.stopPropagation()}
+ >
+
+
+
Endgueltig loeschen
+
+
+
+
+ Sind Sie sicher, dass Sie {project.name} unwiderruflich loeschen moechten?
+
+
+ Alle Projektdaten, SDK-States und Dokumente werden permanent geloescht. Diese Aktion kann nicht rueckgaengig gemacht werden.
+
+
+
+
+ setStep('choose')}
+ className="flex-1 px-4 py-2.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
+ >
+ Zurueck
+
+
+ {isProcessing ? 'Loesche...' : 'Endgueltig loeschen'}
+
+
+
+
+ )
+ }
return (
-
-
-
{project.name}
-
- {project.status === 'active' ? 'Aktiv' : 'Archiviert'}
-
-
-
- {project.description && (
- {project.description}
- )}
-
-
-
V{String(project.projectVersion).padStart(3, '0')}
-
|
-
-
-
+
+
e.stopPropagation()}
+ >
+
+
-
{project.completionPercentage}%
+
Projekt entfernen
-
|
-
{timeAgo}
-
-
- {project.customerType === 'new' ? 'Neukunde' : 'Bestandskunde'}
+
+ Was moechten Sie mit dem Projekt {project.name} tun?
+
+
+
+ {/* Archive option */}
+
+
+
+
Archivieren
+
+ Projekt wird ausgeblendet, Daten bleiben erhalten. Kann jederzeit wiederhergestellt werden.
+
+
+
+
+ {/* Permanent delete option */}
+
setStep('confirm-delete')}
+ disabled={isProcessing}
+ className="w-full flex items-start gap-4 p-4 rounded-xl border-2 border-gray-200 hover:border-red-300 hover:bg-red-50 transition-all text-left disabled:opacity-50"
+ >
+
+
+
Endgueltig loeschen
+
+ Alle Daten werden unwiderruflich geloescht. Diese Aktion kann nicht rueckgaengig gemacht werden.
+
+
+
+
+
+
+ Abbrechen
+
-
+
)
}
// =============================================================================
-// HELPER
+// PROJECT CARD
// =============================================================================
-function formatTimeAgo(dateStr: string): string {
- const date = new Date(dateStr)
- const now = Date.now()
- const diff = now - date.getTime()
- const seconds = Math.floor(diff / 1000)
- if (seconds < 60) return 'Gerade eben'
- const minutes = Math.floor(seconds / 60)
- if (minutes < 60) return `vor ${minutes} Min`
- const hours = Math.floor(minutes / 60)
- if (hours < 24) return `vor ${hours} Std`
- const days = Math.floor(hours / 24)
- return `vor ${days} Tag${days > 1 ? 'en' : ''}`
+function ProjectCard({
+ project,
+ onClick,
+ onDelete,
+ onRestore,
+}: {
+ project: ProjectInfo
+ onClick: () => void
+ onDelete?: () => void
+ onRestore?: () => void
+}) {
+ const timeAgo = formatTimeAgo(project.updatedAt)
+ const isArchived = project.status === 'archived'
+
+ return (
+
+ {/* Action buttons */}
+
+ {isArchived && onRestore && (
+
{
+ e.stopPropagation()
+ onRestore()
+ }}
+ className="p-1.5 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors"
+ title="Wiederherstellen"
+ >
+
+
+
+
+ )}
+ {onDelete && (
+
{
+ e.stopPropagation()
+ onDelete()
+ }}
+ className="p-1.5 text-gray-300 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
+ title="Entfernen"
+ >
+
+
+
+
+ )}
+
+
+ {/* Card content */}
+
+
+
+ {project.name}
+
+
+ {isArchived ? 'Archiviert' : 'Aktiv'}
+
+
+
+ {project.description && (
+ {project.description}
+ )}
+
+
+
V{String(project.projectVersion).padStart(3, '0')}
+
|
+
+
+
{project.completionPercentage}%
+
+ {timeAgo && (
+ <>
+
|
+
{timeAgo}
+ >
+ )}
+
+
+
+ {project.customerType === 'new' ? 'Neukunde' : 'Bestandskunde'}
+
+
+
+ )
}
// =============================================================================
@@ -239,10 +454,14 @@ function formatTimeAgo(dateStr: string): string {
export function ProjectSelector() {
const router = useRouter()
- const { listProjects } = useSDK()
+ const { listProjects, archiveProject, restoreProject, permanentlyDeleteProject } = useSDK()
const [projects, setProjects] = useState
([])
+ const [archivedProjects, setArchivedProjects] = useState([])
const [loading, setLoading] = useState(true)
const [showCreateDialog, setShowCreateDialog] = useState(false)
+ const [actionTarget, setActionTarget] = useState(null)
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [showArchived, setShowArchived] = useState(false)
useEffect(() => {
loadProjects()
@@ -252,7 +471,9 @@ export function ProjectSelector() {
setLoading(true)
try {
const result = await listProjects()
- setProjects(result)
+ const all = result.map(normalizeProject)
+ setProjects(all.filter(p => p.status === 'active'))
+ setArchivedProjects(all.filter(p => p.status === 'archived'))
} catch (error) {
console.error('Failed to load projects:', error)
} finally {
@@ -261,6 +482,7 @@ export function ProjectSelector() {
}
const handleProjectClick = (project: ProjectInfo) => {
+ if (project.status === 'archived') return // archived projects are read-only in list
router.push(`/sdk?project=${project.id}`)
}
@@ -268,6 +490,47 @@ export function ProjectSelector() {
router.push(`/sdk?project=${project.id}`)
}
+ const handleArchive = async () => {
+ if (!actionTarget) return
+ setIsProcessing(true)
+ try {
+ await archiveProject(actionTarget.id)
+ // Move from active to archived
+ setProjects(prev => prev.filter(p => p.id !== actionTarget.id))
+ setArchivedProjects(prev => [...prev, { ...actionTarget, status: 'archived' as const }])
+ setActionTarget(null)
+ } catch (error) {
+ console.error('Failed to archive project:', error)
+ } finally {
+ setIsProcessing(false)
+ }
+ }
+
+ const handlePermanentDelete = async () => {
+ if (!actionTarget) return
+ setIsProcessing(true)
+ try {
+ await permanentlyDeleteProject(actionTarget.id)
+ setProjects(prev => prev.filter(p => p.id !== actionTarget.id))
+ setArchivedProjects(prev => prev.filter(p => p.id !== actionTarget.id))
+ setActionTarget(null)
+ } catch (error) {
+ console.error('Failed to delete project:', error)
+ } finally {
+ setIsProcessing(false)
+ }
+ }
+
+ const handleRestore = async (project: ProjectInfo) => {
+ try {
+ await restoreProject(project.id)
+ setArchivedProjects(prev => prev.filter(p => p.id !== project.id))
+ setProjects(prev => [...prev, { ...project, status: 'active' as const }])
+ } catch (error) {
+ console.error('Failed to restore project:', error)
+ }
+ }
+
return (
{/* Header */}
@@ -297,7 +560,7 @@ export function ProjectSelector() {
)}
{/* Empty State */}
- {!loading && projects.length === 0 && (
+ {!loading && projects.length === 0 && archivedProjects.length === 0 && (
@@ -321,7 +584,7 @@ export function ProjectSelector() {
)}
- {/* Project Grid */}
+ {/* Active Projects */}
{!loading && projects.length > 0 && (
{projects.map(project => (
@@ -329,11 +592,68 @@ export function ProjectSelector() {
key={project.id}
project={project}
onClick={() => handleProjectClick(project)}
+ onDelete={() => setActionTarget(project)}
/>
))}
)}
+ {/* No active but has archived */}
+ {!loading && projects.length === 0 && archivedProjects.length > 0 && (
+
+
Keine aktiven Projekte
+
+ Sie haben {archivedProjects.length} archivierte{archivedProjects.length === 1 ? 's' : ''} Projekt{archivedProjects.length === 1 ? '' : 'e'}.
+ Stellen Sie ein Projekt wieder her oder erstellen Sie ein neues.
+
+
setShowCreateDialog(true)}
+ className="mt-4 inline-flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
+ >
+
+
+
+ Neues Projekt erstellen
+
+
+ )}
+
+ {/* Archived Projects Section */}
+ {!loading && archivedProjects.length > 0 && (
+
+
setShowArchived(!showArchived)}
+ className="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-700 transition-colors mb-4"
+ >
+
+
+
+
+
+
+ Archivierte Projekte ({archivedProjects.length})
+
+
+ {showArchived && (
+
+ {archivedProjects.map(project => (
+
handleProjectClick(project)}
+ onRestore={() => handleRestore(project)}
+ onDelete={() => setActionTarget(project)}
+ />
+ ))}
+
+ )}
+
+ )}
+
{/* Create Dialog */}
+
+ {/* Action Dialog (Archive / Delete) */}
+ {actionTarget && (
+
setActionTarget(null)}
+ isProcessing={isProcessing}
+ />
+ )}
)
}
diff --git a/admin-compliance/lib/sdk/api-client.ts b/admin-compliance/lib/sdk/api-client.ts
index 18480b7..87f148c 100644
--- a/admin-compliance/lib/sdk/api-client.ts
+++ b/admin-compliance/lib/sdk/api-client.ts
@@ -600,9 +600,9 @@ export class SDKApiClient {
/**
* List all projects for the current tenant
*/
- async listProjects(): Promise<{ projects: ProjectInfo[]; total: number }> {
+ async listProjects(includeArchived = true): Promise<{ projects: ProjectInfo[]; total: number }> {
const response = await this.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
- `${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
+ `${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}&include_archived=${includeArchived}`,
{
method: 'GET',
headers: {
@@ -664,6 +664,23 @@ export class SDKApiClient {
return response
}
+ /**
+ * Get a single project by ID
+ */
+ async getProject(projectId: string): Promise
{
+ const response = await this.fetchWithRetry(
+ `${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Tenant-ID': this.tenantId,
+ },
+ }
+ )
+ return response
+ }
+
/**
* Archive (soft-delete) a project
*/
@@ -680,6 +697,39 @@ export class SDKApiClient {
)
}
+ /**
+ * Restore an archived project
+ */
+ async restoreProject(projectId: string): Promise {
+ const response = await this.fetchWithRetry(
+ `${this.baseUrl}/projects/${projectId}/restore?tenant_id=${encodeURIComponent(this.tenantId)}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Tenant-ID': this.tenantId,
+ },
+ }
+ )
+ return response
+ }
+
+ /**
+ * Permanently delete a project and all data
+ */
+ async permanentlyDeleteProject(projectId: string): Promise {
+ await this.fetchWithRetry<{ success: boolean }>(
+ `${this.baseUrl}/projects/${projectId}/permanent?tenant_id=${encodeURIComponent(this.tenantId)}`,
+ {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Tenant-ID': this.tenantId,
+ },
+ }
+ )
+ }
+
/**
* Health check
*/
diff --git a/admin-compliance/lib/sdk/context.tsx b/admin-compliance/lib/sdk/context.tsx
index 7c27d46..08df34a 100644
--- a/admin-compliance/lib/sdk/context.tsx
+++ b/admin-compliance/lib/sdk/context.tsx
@@ -560,6 +560,8 @@ interface SDKContextValue {
listProjects: () => Promise
switchProject: (projectId: string) => void
archiveProject: (projectId: string) => Promise
+ restoreProject: (projectId: string) => Promise
+ permanentlyDeleteProject: (projectId: string) => Promise
}
const SDKContext = createContext(null)
@@ -580,7 +582,7 @@ interface SDKProviderProps {
export function SDKProvider({
children,
- tenantId = 'default',
+ tenantId = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e',
userId = 'default',
projectId,
enableBackendSync = false,
@@ -713,6 +715,16 @@ export function SDKProvider({
}
}
}
+
+ // Load project metadata (name, status, etc.) from backend
+ if (enableBackendSync && projectId && apiClientRef.current) {
+ try {
+ const info = await apiClientRef.current.getProject(projectId)
+ dispatch({ type: 'SET_STATE', payload: { projectInfo: info } })
+ } catch (err) {
+ console.warn('Failed to load project info:', err)
+ }
+ }
} catch (error) {
console.error('Failed to load SDK state:', error)
}
@@ -1101,6 +1113,9 @@ export function SDKProvider({
// Project Management
const createProject = useCallback(
async (name: string, customerType: CustomerType, copyFromProjectId?: string): Promise => {
+ if (!apiClientRef.current && enableBackendSync) {
+ apiClientRef.current = getSDKApiClient(tenantId, projectId)
+ }
if (!apiClientRef.current) {
throw new Error('Backend sync not enabled')
}
@@ -1110,16 +1125,20 @@ export function SDKProvider({
copy_from_project_id: copyFromProjectId,
})
},
- []
+ [enableBackendSync, tenantId, projectId]
)
const listProjectsFn = useCallback(async (): Promise => {
+ // Ensure API client exists (may not be set yet if useEffect hasn't fired)
+ if (!apiClientRef.current && enableBackendSync) {
+ apiClientRef.current = getSDKApiClient(tenantId, projectId)
+ }
if (!apiClientRef.current) {
return []
}
const result = await apiClientRef.current.listProjects()
return result.projects
- }, [])
+ }, [enableBackendSync, tenantId, projectId])
const switchProject = useCallback(
(newProjectId: string) => {
@@ -1133,12 +1152,41 @@ export function SDKProvider({
const archiveProjectFn = useCallback(
async (archiveId: string): Promise => {
+ if (!apiClientRef.current && enableBackendSync) {
+ apiClientRef.current = getSDKApiClient(tenantId, projectId)
+ }
if (!apiClientRef.current) {
throw new Error('Backend sync not enabled')
}
await apiClientRef.current.archiveProject(archiveId)
},
- []
+ [enableBackendSync, tenantId, projectId]
+ )
+
+ const restoreProjectFn = useCallback(
+ async (restoreId: string): Promise => {
+ if (!apiClientRef.current && enableBackendSync) {
+ apiClientRef.current = getSDKApiClient(tenantId, projectId)
+ }
+ if (!apiClientRef.current) {
+ throw new Error('Backend sync not enabled')
+ }
+ return apiClientRef.current.restoreProject(restoreId)
+ },
+ [enableBackendSync, tenantId, projectId]
+ )
+
+ const permanentlyDeleteProjectFn = useCallback(
+ async (deleteId: string): Promise => {
+ if (!apiClientRef.current && enableBackendSync) {
+ apiClientRef.current = getSDKApiClient(tenantId, projectId)
+ }
+ if (!apiClientRef.current) {
+ throw new Error('Backend sync not enabled')
+ }
+ await apiClientRef.current.permanentlyDeleteProject(deleteId)
+ },
+ [enableBackendSync, tenantId, projectId]
)
// Export
@@ -1206,6 +1254,8 @@ export function SDKProvider({
listProjects: listProjectsFn,
switchProject,
archiveProject: archiveProjectFn,
+ restoreProject: restoreProjectFn,
+ permanentlyDeleteProject: permanentlyDeleteProjectFn,
}
return {children}
diff --git a/backend-compliance/compliance/api/project_routes.py b/backend-compliance/compliance/api/project_routes.py
index d08fec3..7e511de 100644
--- a/backend-compliance/compliance/api/project_routes.py
+++ b/backend-compliance/compliance/api/project_routes.py
@@ -179,7 +179,7 @@ async def create_project(
db.execute(
text("""
INSERT INTO sdk_states (tenant_id, project_id, state, version, created_at, updated_at)
- VALUES (:tenant_id, :project_id, :state::jsonb, 1, NOW(), NOW())
+ VALUES (:tenant_id, :project_id, CAST(:state AS jsonb), 1, NOW(), NOW())
"""),
{
"tenant_id": tenant_id,
@@ -298,3 +298,79 @@ async def archive_project(
raise
finally:
db.close()
+
+
+@router.post("/{project_id}/restore")
+async def restore_project(
+ project_id: str,
+ tenant_id: str = Depends(get_tenant_id),
+):
+ """Restore an archived project back to active."""
+ db = SessionLocal()
+ try:
+ result = db.execute(
+ text("""
+ UPDATE compliance_projects
+ SET status = 'active', archived_at = NULL, updated_at = NOW()
+ WHERE id = :project_id AND tenant_id = :tenant_id AND status = 'archived'
+ RETURNING id, tenant_id, name, description, customer_type, status,
+ project_version, completion_percentage, created_at, updated_at
+ """),
+ {"project_id": project_id, "tenant_id": tenant_id},
+ )
+ row = result.fetchone()
+ if not row:
+ raise HTTPException(status_code=404, detail="Project not found or not archived")
+ db.commit()
+ logger.info("Restored project %s for tenant %s", project_id, tenant_id)
+ return _row_to_response(row)
+ except HTTPException:
+ raise
+ except Exception:
+ db.rollback()
+ raise
+ finally:
+ db.close()
+
+
+@router.delete("/{project_id}/permanent")
+async def permanently_delete_project(
+ project_id: str,
+ tenant_id: str = Depends(get_tenant_id),
+):
+ """Permanently delete a project and all associated data."""
+ db = SessionLocal()
+ try:
+ # Verify project exists and belongs to tenant
+ check = db.execute(
+ text("""
+ SELECT id FROM compliance_projects
+ WHERE id = :project_id AND tenant_id = :tenant_id
+ """),
+ {"project_id": project_id, "tenant_id": tenant_id},
+ ).fetchone()
+ if not check:
+ raise HTTPException(status_code=404, detail="Project not found")
+
+ # Delete sdk_states (CASCADE should handle this, but be explicit)
+ db.execute(
+ text("DELETE FROM sdk_states WHERE project_id = :project_id AND tenant_id = :tenant_id"),
+ {"project_id": project_id, "tenant_id": tenant_id},
+ )
+
+ # Delete the project itself
+ db.execute(
+ text("DELETE FROM compliance_projects WHERE id = :project_id AND tenant_id = :tenant_id"),
+ {"project_id": project_id, "tenant_id": tenant_id},
+ )
+
+ db.commit()
+ logger.info("Permanently deleted project %s for tenant %s", project_id, tenant_id)
+ return {"success": True, "id": project_id, "status": "deleted"}
+ except HTTPException:
+ raise
+ except Exception:
+ db.rollback()
+ raise
+ finally:
+ db.close()