Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
540 lines
14 KiB
TypeScript
540 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import React, {
|
|
createContext,
|
|
useContext,
|
|
useReducer,
|
|
useEffect,
|
|
useCallback,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react'
|
|
import {
|
|
ComplianceClient,
|
|
sdkReducer,
|
|
initialState,
|
|
StateSyncManager,
|
|
createStateSyncManager,
|
|
createDSGVOModule,
|
|
createComplianceModule,
|
|
createRAGModule,
|
|
createSecurityModule,
|
|
type DSGVOModule,
|
|
type ComplianceModule,
|
|
type RAGModule,
|
|
type SecurityModule,
|
|
} from '@breakpilot/compliance-sdk-core'
|
|
import type {
|
|
SDKState,
|
|
SDKAction,
|
|
SDKStep,
|
|
CheckpointStatus,
|
|
SyncState,
|
|
UseCaseAssessment,
|
|
Risk,
|
|
Control,
|
|
UserPreferences,
|
|
} from '@breakpilot/compliance-sdk-types'
|
|
import {
|
|
getStepById,
|
|
getNextStep,
|
|
getPreviousStep,
|
|
getCompletionPercentage,
|
|
getPhaseCompletionPercentage,
|
|
} from '@breakpilot/compliance-sdk-types'
|
|
|
|
// =============================================================================
|
|
// CONTEXT TYPES
|
|
// =============================================================================
|
|
|
|
export interface ComplianceContextValue {
|
|
// State
|
|
state: SDKState
|
|
dispatch: React.Dispatch<SDKAction>
|
|
|
|
// Client
|
|
client: ComplianceClient
|
|
|
|
// Modules
|
|
dsgvo: DSGVOModule
|
|
compliance: ComplianceModule
|
|
rag: RAGModule
|
|
security: SecurityModule
|
|
|
|
// Navigation
|
|
currentStep: SDKStep | undefined
|
|
goToStep: (stepId: string) => void
|
|
goToNextStep: () => void
|
|
goToPreviousStep: () => void
|
|
canGoNext: boolean
|
|
canGoPrevious: boolean
|
|
|
|
// Progress
|
|
completionPercentage: number
|
|
phase1Completion: number
|
|
phase2Completion: number
|
|
|
|
// Checkpoints
|
|
validateCheckpoint: (checkpointId: string) => Promise<CheckpointStatus>
|
|
overrideCheckpoint: (checkpointId: string, reason: string) => Promise<void>
|
|
getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined
|
|
|
|
// State Updates
|
|
updateUseCase: (id: string, data: Partial<UseCaseAssessment>) => void
|
|
addRisk: (risk: Risk) => void
|
|
updateControl: (id: string, data: Partial<Control>) => void
|
|
|
|
// Persistence
|
|
saveState: () => Promise<void>
|
|
loadState: () => Promise<void>
|
|
resetState: () => void
|
|
|
|
// Sync
|
|
syncState: SyncState
|
|
forceSyncToServer: () => Promise<void>
|
|
isOnline: boolean
|
|
|
|
// Export
|
|
exportState: (format: 'json' | 'pdf' | 'zip') => Promise<Blob>
|
|
|
|
// Command Bar
|
|
isCommandBarOpen: boolean
|
|
setCommandBarOpen: (open: boolean) => void
|
|
|
|
// Status
|
|
isInitialized: boolean
|
|
isLoading: boolean
|
|
error: Error | null
|
|
}
|
|
|
|
export const ComplianceContext = createContext<ComplianceContextValue | null>(null)
|
|
|
|
// =============================================================================
|
|
// PROVIDER PROPS
|
|
// =============================================================================
|
|
|
|
export interface ComplianceProviderProps {
|
|
children: React.ReactNode
|
|
apiEndpoint: string
|
|
apiKey?: string
|
|
tenantId: string
|
|
userId?: string
|
|
enableBackendSync?: boolean
|
|
onNavigate?: (url: string) => void
|
|
onError?: (error: Error) => void
|
|
}
|
|
|
|
// =============================================================================
|
|
// PROVIDER
|
|
// =============================================================================
|
|
|
|
const SDK_STORAGE_KEY = 'breakpilot-compliance-sdk-state'
|
|
|
|
export function ComplianceProvider({
|
|
children,
|
|
apiEndpoint,
|
|
apiKey,
|
|
tenantId,
|
|
userId = 'default',
|
|
enableBackendSync = true,
|
|
onNavigate,
|
|
onError,
|
|
}: ComplianceProviderProps) {
|
|
const [state, dispatch] = useReducer(sdkReducer, {
|
|
...initialState,
|
|
tenantId,
|
|
userId,
|
|
})
|
|
const [isCommandBarOpen, setCommandBarOpen] = useState(false)
|
|
const [isInitialized, setIsInitialized] = useState(false)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<Error | null>(null)
|
|
const [syncState, setSyncState] = useState<SyncState>({
|
|
status: 'idle',
|
|
lastSyncedAt: null,
|
|
localVersion: 0,
|
|
serverVersion: 0,
|
|
pendingChanges: 0,
|
|
error: null,
|
|
})
|
|
const [isOnline, setIsOnline] = useState(true)
|
|
|
|
// Refs
|
|
const clientRef = useRef<ComplianceClient | null>(null)
|
|
const syncManagerRef = useRef<StateSyncManager | null>(null)
|
|
|
|
// Initialize client
|
|
if (!clientRef.current) {
|
|
clientRef.current = new ComplianceClient({
|
|
apiEndpoint,
|
|
apiKey,
|
|
tenantId,
|
|
onError: err => {
|
|
setError(err)
|
|
onError?.(err)
|
|
},
|
|
})
|
|
}
|
|
|
|
const client = clientRef.current
|
|
|
|
// Modules
|
|
const dsgvo = useMemo(
|
|
() => createDSGVOModule(client, () => state),
|
|
[client, state]
|
|
)
|
|
const compliance = useMemo(
|
|
() => createComplianceModule(client, () => state),
|
|
[client, state]
|
|
)
|
|
const rag = useMemo(() => createRAGModule(client), [client])
|
|
const security = useMemo(
|
|
() => createSecurityModule(client, () => state),
|
|
[client, state]
|
|
)
|
|
|
|
// Initialize sync manager
|
|
useEffect(() => {
|
|
if (enableBackendSync && typeof window !== 'undefined') {
|
|
syncManagerRef.current = createStateSyncManager(
|
|
client,
|
|
tenantId,
|
|
{ debounceMs: 2000, maxRetries: 3 },
|
|
{
|
|
onSyncStart: () => {
|
|
setSyncState(prev => ({ ...prev, status: 'syncing' }))
|
|
},
|
|
onSyncComplete: syncedState => {
|
|
setSyncState(prev => ({
|
|
...prev,
|
|
status: 'idle',
|
|
lastSyncedAt: new Date(),
|
|
pendingChanges: 0,
|
|
}))
|
|
if (new Date(syncedState.lastModified) > new Date(state.lastModified)) {
|
|
dispatch({ type: 'SET_STATE', payload: syncedState })
|
|
}
|
|
},
|
|
onSyncError: err => {
|
|
setSyncState(prev => ({
|
|
...prev,
|
|
status: 'error',
|
|
error: err.message,
|
|
}))
|
|
setError(err)
|
|
},
|
|
onConflict: () => {
|
|
setSyncState(prev => ({ ...prev, status: 'conflict' }))
|
|
},
|
|
onOffline: () => {
|
|
setIsOnline(false)
|
|
setSyncState(prev => ({ ...prev, status: 'offline' }))
|
|
},
|
|
onOnline: () => {
|
|
setIsOnline(true)
|
|
setSyncState(prev => ({ ...prev, status: 'idle' }))
|
|
},
|
|
}
|
|
)
|
|
}
|
|
|
|
return () => {
|
|
syncManagerRef.current?.destroy()
|
|
}
|
|
}, [enableBackendSync, tenantId, client])
|
|
|
|
// Load initial state
|
|
useEffect(() => {
|
|
const loadInitialState = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
// Load from localStorage first
|
|
if (typeof window !== 'undefined') {
|
|
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored)
|
|
if (parsed.lastModified) {
|
|
parsed.lastModified = new Date(parsed.lastModified)
|
|
}
|
|
dispatch({ type: 'SET_STATE', payload: parsed })
|
|
}
|
|
}
|
|
|
|
// Then load from server if enabled
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
const serverState = await syncManagerRef.current.loadFromServer()
|
|
if (serverState) {
|
|
dispatch({ type: 'SET_STATE', payload: serverState })
|
|
}
|
|
}
|
|
} catch (err) {
|
|
setError(err as Error)
|
|
onError?.(err as Error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
setIsInitialized(true)
|
|
}
|
|
}
|
|
|
|
loadInitialState()
|
|
}, [tenantId, enableBackendSync])
|
|
|
|
// Auto-save
|
|
useEffect(() => {
|
|
if (!isInitialized || !state.preferences.autoSave) return
|
|
|
|
const saveTimeout = setTimeout(() => {
|
|
try {
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
|
}
|
|
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
syncManagerRef.current.queueSync(state)
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to save state:', err)
|
|
}
|
|
}, 1000)
|
|
|
|
return () => clearTimeout(saveTimeout)
|
|
}, [state, tenantId, isInitialized, enableBackendSync])
|
|
|
|
// Keyboard shortcuts
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
e.preventDefault()
|
|
setCommandBarOpen(prev => !prev)
|
|
}
|
|
if (e.key === 'Escape' && isCommandBarOpen) {
|
|
setCommandBarOpen(false)
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
}, [isCommandBarOpen])
|
|
|
|
// Navigation
|
|
const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep])
|
|
|
|
const goToStep = useCallback(
|
|
(stepId: string) => {
|
|
const step = getStepById(stepId)
|
|
if (step) {
|
|
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
|
|
onNavigate?.(step.url)
|
|
}
|
|
},
|
|
[onNavigate]
|
|
)
|
|
|
|
const goToNextStep = useCallback(() => {
|
|
const nextStep = getNextStep(state.currentStep)
|
|
if (nextStep) {
|
|
goToStep(nextStep.id)
|
|
}
|
|
}, [state.currentStep, goToStep])
|
|
|
|
const goToPreviousStep = useCallback(() => {
|
|
const prevStep = getPreviousStep(state.currentStep)
|
|
if (prevStep) {
|
|
goToStep(prevStep.id)
|
|
}
|
|
}, [state.currentStep, goToStep])
|
|
|
|
const canGoNext = useMemo(() => getNextStep(state.currentStep) !== undefined, [state.currentStep])
|
|
const canGoPrevious = useMemo(
|
|
() => getPreviousStep(state.currentStep) !== undefined,
|
|
[state.currentStep]
|
|
)
|
|
|
|
// Progress
|
|
const completionPercentage = useMemo(() => getCompletionPercentage(state), [state])
|
|
const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state])
|
|
const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state])
|
|
|
|
// Checkpoints
|
|
const validateCheckpoint = useCallback(
|
|
async (checkpointId: string): Promise<CheckpointStatus> => {
|
|
if (enableBackendSync) {
|
|
try {
|
|
const result = await client.validateCheckpoint(checkpointId, state)
|
|
const status: CheckpointStatus = {
|
|
checkpointId: result.checkpointId,
|
|
passed: result.passed,
|
|
validatedAt: new Date(result.validatedAt),
|
|
validatedBy: result.validatedBy,
|
|
errors: result.errors,
|
|
warnings: result.warnings,
|
|
}
|
|
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
|
return status
|
|
} catch {
|
|
// Fall through to local validation
|
|
}
|
|
}
|
|
|
|
// Local validation
|
|
const status: CheckpointStatus = {
|
|
checkpointId,
|
|
passed: true,
|
|
validatedAt: new Date(),
|
|
validatedBy: 'SYSTEM',
|
|
errors: [],
|
|
warnings: [],
|
|
}
|
|
|
|
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
|
return status
|
|
},
|
|
[state, enableBackendSync, client]
|
|
)
|
|
|
|
const overrideCheckpoint = useCallback(
|
|
async (checkpointId: string, reason: string): Promise<void> => {
|
|
const existingStatus = state.checkpoints[checkpointId]
|
|
const overriddenStatus: CheckpointStatus = {
|
|
...existingStatus,
|
|
checkpointId,
|
|
passed: true,
|
|
overrideReason: reason,
|
|
overriddenBy: state.userId,
|
|
overriddenAt: new Date(),
|
|
errors: [],
|
|
warnings: existingStatus?.warnings || [],
|
|
}
|
|
|
|
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overriddenStatus } })
|
|
},
|
|
[state.checkpoints, state.userId]
|
|
)
|
|
|
|
const getCheckpointStatus = useCallback(
|
|
(checkpointId: string): CheckpointStatus | undefined => {
|
|
return state.checkpoints[checkpointId]
|
|
},
|
|
[state.checkpoints]
|
|
)
|
|
|
|
// State Updates
|
|
const updateUseCase = useCallback((id: string, data: Partial<UseCaseAssessment>) => {
|
|
dispatch({ type: 'UPDATE_USE_CASE', payload: { id, data } })
|
|
}, [])
|
|
|
|
const addRisk = useCallback((risk: Risk) => {
|
|
dispatch({ type: 'ADD_RISK', payload: risk })
|
|
}, [])
|
|
|
|
const updateControl = useCallback((id: string, data: Partial<Control>) => {
|
|
dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } })
|
|
}, [])
|
|
|
|
// Persistence
|
|
const saveState = useCallback(async (): Promise<void> => {
|
|
try {
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
|
}
|
|
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
await syncManagerRef.current.forceSync(state)
|
|
}
|
|
} catch (err) {
|
|
setError(err as Error)
|
|
throw err
|
|
}
|
|
}, [state, tenantId, enableBackendSync])
|
|
|
|
const loadState = useCallback(async (): Promise<void> => {
|
|
setIsLoading(true)
|
|
try {
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
const serverState = await syncManagerRef.current.loadFromServer()
|
|
if (serverState) {
|
|
dispatch({ type: 'SET_STATE', payload: serverState })
|
|
return
|
|
}
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
|
if (stored) {
|
|
dispatch({ type: 'SET_STATE', payload: JSON.parse(stored) })
|
|
}
|
|
}
|
|
} catch (err) {
|
|
setError(err as Error)
|
|
throw err
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [tenantId, enableBackendSync])
|
|
|
|
const resetState = useCallback(() => {
|
|
dispatch({ type: 'RESET_STATE' })
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
|
}
|
|
}, [tenantId])
|
|
|
|
// Sync
|
|
const forceSyncToServer = useCallback(async (): Promise<void> => {
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
await syncManagerRef.current.forceSync(state)
|
|
}
|
|
}, [state, enableBackendSync])
|
|
|
|
// Export
|
|
const exportState = useCallback(
|
|
async (format: 'json' | 'pdf' | 'zip'): Promise<Blob> => {
|
|
if (format === 'json') {
|
|
return new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
|
}
|
|
return client.exportState(format)
|
|
},
|
|
[state, client]
|
|
)
|
|
|
|
const value: ComplianceContextValue = {
|
|
state,
|
|
dispatch,
|
|
client,
|
|
dsgvo,
|
|
compliance,
|
|
rag,
|
|
security,
|
|
currentStep,
|
|
goToStep,
|
|
goToNextStep,
|
|
goToPreviousStep,
|
|
canGoNext,
|
|
canGoPrevious,
|
|
completionPercentage,
|
|
phase1Completion,
|
|
phase2Completion,
|
|
validateCheckpoint,
|
|
overrideCheckpoint,
|
|
getCheckpointStatus,
|
|
updateUseCase,
|
|
addRisk,
|
|
updateControl,
|
|
saveState,
|
|
loadState,
|
|
resetState,
|
|
syncState,
|
|
forceSyncToServer,
|
|
isOnline,
|
|
exportState,
|
|
isCommandBarOpen,
|
|
setCommandBarOpen,
|
|
isInitialized,
|
|
isLoading,
|
|
error,
|
|
}
|
|
|
|
return <ComplianceContext.Provider value={value}>{children}</ComplianceContext.Provider>
|
|
}
|