/* ═══════════════════════════════════════════════════════════════ ScanContext — Global state management for the scan lifecycle ═══════════════════════════════════════════════════════════════ */ import { createContext, useContext, useReducer, useCallback, useRef } from 'react'; import { createScanService } from '../services/scanService'; const ScanContext = createContext(null); // View states export const VIEWS = { LANDING: 'landing', ANALYSIS: 'analysis', REPORT: 'report', }; // Scan states export const SCAN_STATUS = { IDLE: 'idle', SCANNING: 'scanning', COMPLETE: 'complete', ERROR: 'error', }; // Agent states export const AGENT_STATUS = { IDLE: 'idle', SCANNING: 'scanning', COMPLETE: 'complete', }; const initialState = { view: VIEWS.LANDING, scanStatus: SCAN_STATUS.IDLE, scanId: null, findings: [], fixes: [], agents: { security: { status: AGENT_STATUS.IDLE, progress: 0, findingsCount: 0, filesScanned: 0, message: '' }, performance: { status: AGENT_STATUS.IDLE, progress: 0, findingsCount: 0, filesScanned: 0, message: '' }, fix: { status: AGENT_STATUS.IDLE, progress: 0, findingsCount: 0, filesScanned: 0, message: '' }, }, summary: null, error: null, startTime: null, elapsedTime: 0, amdMetrics: null, amdMigration: null, }; function scanReducer(state, action) { switch (action.type) { case 'SET_VIEW': return { ...state, view: action.payload }; case 'SCAN_STARTED': return { ...state, view: VIEWS.ANALYSIS, scanStatus: SCAN_STATUS.SCANNING, scanId: action.payload.scanId, startTime: Date.now(), findings: [], fixes: [], error: null, summary: null, amdMetrics: null, amdMigration: null, agents: { security: { ...initialState.agents.security }, performance: { ...initialState.agents.performance }, fix: { ...initialState.agents.fix }, }, }; case 'AGENT_START': return { ...state, agents: { ...state.agents, [action.payload.agent]: { ...state.agents[action.payload.agent], status: AGENT_STATUS.SCANNING, message: action.payload.message || 'Initializing...', }, }, }; case 'PROGRESS': return { ...state, agents: { ...state.agents, [action.payload.agent]: { ...state.agents[action.payload.agent], progress: action.payload.percent, filesScanned: action.payload.filesScanned || state.agents[action.payload.agent].filesScanned, message: action.payload.message || state.agents[action.payload.agent].message, status: action.payload.percent >= 100 ? AGENT_STATUS.COMPLETE : AGENT_STATUS.SCANNING, }, }, }; case 'FINDING': { const agent = action.payload.agent; return { ...state, findings: [...state.findings, action.payload], agents: { ...state.agents, [agent]: { ...state.agents[agent], findingsCount: state.agents[agent].findingsCount + 1, }, }, }; } case 'FIX_READY': return { ...state, fixes: [...state.fixes, action.payload], agents: { ...state.agents, fix: { ...state.agents.fix, findingsCount: state.agents.fix.findingsCount + 1, }, }, }; case 'COMPLETE': return { ...state, scanStatus: SCAN_STATUS.COMPLETE, summary: action.payload, agents: { ...state.agents, security: { ...state.agents.security, status: AGENT_STATUS.COMPLETE, progress: 100 }, performance: { ...state.agents.performance, status: AGENT_STATUS.COMPLETE, progress: 100 }, fix: { ...state.agents.fix, status: AGENT_STATUS.COMPLETE, progress: 100 }, }, }; case 'ERROR': return { ...state, scanStatus: SCAN_STATUS.ERROR, error: action.payload.message, }; case 'UPDATE_ELAPSED': return { ...state, elapsedTime: action.payload, }; case 'AMD_METRICS': return { ...state, amdMetrics: action.payload, }; case 'AMD_MIGRATION_FINDING': { const prev = state.amdMigration || { findings: [], compatibility_score: 100, compatibility_label: 'Analyzing...', total_cuda_patterns_found: 0, summary: '' }; return { ...state, amdMigration: { ...prev, findings: [...prev.findings, action.payload], total_cuda_patterns_found: prev.total_cuda_patterns_found + 1, }, }; } case 'AMD_MIGRATION_SUMMARY': return { ...state, amdMigration: { ...(state.amdMigration || { findings: [] }), ...action.payload, }, }; case 'RESET': return { ...initialState }; default: return state; } } export function ScanProvider({ children }) { const [state, dispatch] = useReducer(scanReducer, initialState); const serviceRef = useRef(null); const timerRef = useRef(null); const startScan = useCallback(async (payload) => { // Cleanup previous scan if (serviceRef.current) { serviceRef.current.destroy(); } if (timerRef.current) { clearInterval(timerRef.current); } const service = createScanService(); serviceRef.current = service; // Wire up event handlers service.on('scan_started', (data) => { dispatch({ type: 'SCAN_STARTED', payload: data }); // Start elapsed time counter const start = Date.now(); timerRef.current = setInterval(() => { dispatch({ type: 'UPDATE_ELAPSED', payload: Date.now() - start }); }, 100); }); service.on('agent_start', (data) => { dispatch({ type: 'AGENT_START', payload: data }); }); service.on('progress', (data) => { dispatch({ type: 'PROGRESS', payload: data }); }); service.on('finding', (data) => { dispatch({ type: 'FINDING', payload: data }); }); service.on('fix_ready', (data) => { dispatch({ type: 'FIX_READY', payload: data }); }); service.on('amd_metrics', (data) => { dispatch({ type: 'AMD_METRICS', payload: data }); }); service.on('amd_migration_finding', (data) => { dispatch({ type: 'AMD_MIGRATION_FINDING', payload: data }); }); service.on('amd_migration_summary', (data) => { dispatch({ type: 'AMD_MIGRATION_SUMMARY', payload: data }); }); service.on('complete', (data) => { console.log('[ScanContext] COMPLETE event received — triggering notification'); dispatch({ type: 'COMPLETE', payload: data }); if (timerRef.current) { clearInterval(timerRef.current); } // Handle Notification / Toast console.log("[ScanContext] Notification permission status:", Notification.permission); if ('Notification' in window && Notification.permission === 'granted') { try { console.log("[ScanContext] Attempting to show native notification..."); const notification = new Notification("✅ CodeSentry Analysis Complete", { body: "Your code scan is done. Open the report to review findings.", tag: "codesentry-scan-complete", // Prevents duplicate notifications requireInteraction: true // Keeps it visible until user interacts }); notification.onclick = function() { window.focus(); dispatch({ type: 'SET_VIEW', payload: VIEWS.REPORT }); this.close(); }; } catch (e) { console.error("[ScanContext] Native Notification failed:", e); } } // Always show the in-app toast to guarantee visibility // Play a subtle notification sound try { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); osc.frequency.value = 880; osc.type = 'sine'; gain.gain.value = 0.15; osc.start(); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3); osc.stop(audioCtx.currentTime + 0.3); } catch(e) { /* audio not critical */ } const toast = document.createElement('div'); toast.id = 'codesentry-complete-toast'; toast.innerHTML = `
Your code scan is done. Click to view report.