refactor(sdk): split hooks, dsr-portal, provider, sync approaching 500 LOC
All four files split into focused sibling modules so every file lands
comfortably under the 300-LOC soft target (hard cap 500):
hooks.ts (474→43) → hooks-core / hooks-dsgvo / hooks-compliance
hooks-rag-security / hooks-ui
dsr-portal.ts (464→129) → dsr-portal-translations / dsr-portal-render
provider.tsx (462→247) → provider-effects / provider-callbacks
sync.ts (435→299) → sync-storage / sync-conflict
Zero behaviour changes. All public APIs remain importable from the
original paths (hooks.ts re-exports every hook, provider.tsx keeps all
named exports, sync.ts preserves StateSyncManager + factory).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
|
||||
import React, {
|
||||
useReducer,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -13,19 +11,12 @@ import {
|
||||
sdkReducer,
|
||||
initialState,
|
||||
StateSyncManager,
|
||||
createStateSyncManager,
|
||||
createDSGVOModule,
|
||||
createComplianceModule,
|
||||
createRAGModule,
|
||||
createSecurityModule,
|
||||
} from '@breakpilot/compliance-sdk-core'
|
||||
import type {
|
||||
CheckpointStatus,
|
||||
SyncState,
|
||||
UseCaseAssessment,
|
||||
Risk,
|
||||
Control,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
import type { SyncState } from '@breakpilot/compliance-sdk-types'
|
||||
import {
|
||||
getStepById,
|
||||
getNextStep,
|
||||
@@ -35,10 +26,29 @@ import {
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
import {
|
||||
ComplianceContext,
|
||||
SDK_STORAGE_KEY,
|
||||
type ComplianceContextValue,
|
||||
type ComplianceProviderProps,
|
||||
} from './provider-context'
|
||||
import {
|
||||
useSyncManagerEffect,
|
||||
useLoadInitialStateEffect,
|
||||
useAutoSaveEffect,
|
||||
useKeyboardShortcutsEffect,
|
||||
} from './provider-effects'
|
||||
import {
|
||||
useValidateCheckpoint,
|
||||
useOverrideCheckpoint,
|
||||
useGetCheckpointStatus,
|
||||
useUpdateUseCase,
|
||||
useAddRisk,
|
||||
useUpdateControl,
|
||||
useSaveState,
|
||||
useLoadState,
|
||||
useResetState,
|
||||
useForceSyncToServer,
|
||||
useExportState,
|
||||
} from './provider-callbacks'
|
||||
import { useCallback, useMemo as useMemoReact } from 'react'
|
||||
|
||||
export {
|
||||
ComplianceContext,
|
||||
@@ -69,7 +79,7 @@ export function ComplianceProvider({
|
||||
tenantId,
|
||||
userId,
|
||||
})
|
||||
const [isCommandBarOpen, setCommandBarOpen] = useState(false)
|
||||
const [isCommandBarOpen, setCommandBarOpenRaw] = useState(false)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
@@ -87,7 +97,7 @@ export function ComplianceProvider({
|
||||
const clientRef = useRef<ComplianceClient | null>(null)
|
||||
const syncManagerRef = useRef<StateSyncManager | null>(null)
|
||||
|
||||
// Initialize client
|
||||
// Initialize client (once)
|
||||
if (!clientRef.current) {
|
||||
clientRef.current = new ComplianceClient({
|
||||
apiEndpoint,
|
||||
@@ -103,144 +113,46 @@ export function ComplianceProvider({
|
||||
const client = clientRef.current
|
||||
|
||||
// Modules
|
||||
const dsgvo = useMemo(
|
||||
() => createDSGVOModule(client, () => state),
|
||||
[client, state]
|
||||
)
|
||||
const compliance = useMemo(
|
||||
() => createComplianceModule(client, () => state),
|
||||
[client, state]
|
||||
)
|
||||
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]
|
||||
const security = useMemo(() => createSecurityModule(client, () => state), [client, state])
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Effects (extracted to provider-effects.ts)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const syncStateSetters = useMemo(
|
||||
() => ({ setSyncState, setIsOnline, setError, dispatch }),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
|
||||
// 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' }))
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
useSyncManagerEffect(enableBackendSync, tenantId, client, state, syncManagerRef, syncStateSetters)
|
||||
|
||||
return () => {
|
||||
syncManagerRef.current?.destroy()
|
||||
}
|
||||
}, [enableBackendSync, tenantId, client])
|
||||
useLoadInitialStateEffect(tenantId, enableBackendSync, syncManagerRef, {
|
||||
setIsLoading,
|
||||
setIsInitialized,
|
||||
setError,
|
||||
dispatch,
|
||||
onError,
|
||||
})
|
||||
|
||||
// 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 })
|
||||
}
|
||||
}
|
||||
useAutoSaveEffect(state, tenantId, isInitialized, enableBackendSync, syncManagerRef)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
const setCommandBarOpen = useCallback(
|
||||
(fn: boolean | ((prev: boolean) => boolean)) => {
|
||||
setCommandBarOpenRaw(typeof fn === 'function' ? fn : () => fn)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
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])
|
||||
useKeyboardShortcutsEffect(isCommandBarOpen, setCommandBarOpen as (fn: (prev: boolean) => boolean) => void)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Navigation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep])
|
||||
|
||||
const goToStep = useCallback(
|
||||
@@ -256,16 +168,12 @@ export function ComplianceProvider({
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
const nextStep = getNextStep(state.currentStep)
|
||||
if (nextStep) {
|
||||
goToStep(nextStep.id)
|
||||
}
|
||||
if (nextStep) goToStep(nextStep.id)
|
||||
}, [state.currentStep, goToStep])
|
||||
|
||||
const goToPreviousStep = useCallback(() => {
|
||||
const prevStep = getPreviousStep(state.currentStep)
|
||||
if (prevStep) {
|
||||
goToStep(prevStep.id)
|
||||
}
|
||||
if (prevStep) goToStep(prevStep.id)
|
||||
}, [state.currentStep, goToStep])
|
||||
|
||||
const canGoNext = useMemo(() => getNextStep(state.currentStep) !== undefined, [state.currentStep])
|
||||
@@ -274,152 +182,29 @@ export function ComplianceProvider({
|
||||
[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
|
||||
}
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Callbacks (extracted to provider-callbacks.ts)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Local validation
|
||||
const status: CheckpointStatus = {
|
||||
checkpointId,
|
||||
passed: true,
|
||||
validatedAt: new Date(),
|
||||
validatedBy: 'SYSTEM',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
}
|
||||
const validateCheckpoint = useValidateCheckpoint(state, enableBackendSync, client, dispatch)
|
||||
const overrideCheckpoint = useOverrideCheckpoint(state, dispatch)
|
||||
const getCheckpointStatus = useGetCheckpointStatus(state)
|
||||
const updateUseCase = useUpdateUseCase(dispatch)
|
||||
const addRisk = useAddRisk(dispatch)
|
||||
const updateControl = useUpdateControl(dispatch)
|
||||
const saveState = useSaveState(state, tenantId, enableBackendSync, syncManagerRef, e => setError(e))
|
||||
const loadState = useLoadState(tenantId, enableBackendSync, syncManagerRef, setIsLoading, dispatch, e => setError(e))
|
||||
const resetState = useResetState(tenantId, dispatch)
|
||||
const forceSyncToServer = useForceSyncToServer(state, enableBackendSync, syncManagerRef)
|
||||
const exportState = useExportState(state, client)
|
||||
|
||||
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]
|
||||
)
|
||||
// -------------------------------------------------------------------------
|
||||
// Context value
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const value: ComplianceContextValue = {
|
||||
state,
|
||||
@@ -452,7 +237,7 @@ export function ComplianceProvider({
|
||||
isOnline,
|
||||
exportState,
|
||||
isCommandBarOpen,
|
||||
setCommandBarOpen,
|
||||
setCommandBarOpen: setCommandBarOpenRaw,
|
||||
isInitialized,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
Reference in New Issue
Block a user