Spaces:
Paused
Paused
| const { v4: uuidv4 } = require('uuid'); | |
| const { getDatabase } = require('../database/init'); | |
| const AccessibilityService = require('./accessibilityService'); | |
| class AnalysisService { | |
| constructor(dependencies = {}) { | |
| // Use injected dependencies or create fallback instances | |
| this.screenshotService = dependencies.screenshotService; | |
| this.aiCritiqueService = dependencies.aiCritiqueService; | |
| this.visualDesignAnalyzer = dependencies.visualDesignAnalyzer; | |
| this.codeGenerationService = dependencies.codeGenerationService; | |
| this.config = dependencies.config; | |
| // Still create accessibility service directly for now | |
| this.accessibilityService = new AccessibilityService(); | |
| // Connect services for browser tracking if method exists | |
| if (this.screenshotService && typeof this.screenshotService.setAnalysisService === 'function') { | |
| this.screenshotService.setAnalysisService(this); | |
| } | |
| // Use config or environment variables | |
| this.maxConcurrentAnalyses = this.config?.analysis?.maxConcurrent || parseInt(process.env.MAX_CONCURRENT_ANALYSES) || 3; | |
| this.timeoutMs = this.config?.analysis?.timeoutMs || parseInt(process.env.ANALYSIS_TIMEOUT_MS) || 300000; // 5 minutes | |
| this.maxStuckTime = this.config?.analysis?.maxStuckTimeMs || parseInt(process.env.MAX_STUCK_TIME_MS) || 600000; // 10 minutes max | |
| this.activeAnalyses = new Map(); | |
| this.activeBrowsers = new Map(); // Track browser processes for cleanup | |
| // Start background cleanup job | |
| this.startCleanupJob(); | |
| } | |
| startCleanupJob() { | |
| // Run cleanup every 2 minutes | |
| this.cleanupInterval = setInterval(() => { | |
| this.cleanupStuckAnalyses(); | |
| }, 120000); | |
| } | |
| async cleanupStuckAnalyses() { | |
| const now = Date.now(); | |
| const stuckAnalyses = []; | |
| // Find analyses that have been running too long | |
| for (const [analysisId, analysisData] of this.activeAnalyses.entries()) { | |
| const runtime = now - analysisData.startTime; | |
| if (runtime > this.maxStuckTime) { | |
| stuckAnalyses.push(analysisId); | |
| } | |
| } | |
| // Clean up stuck analyses | |
| for (const analysisId of stuckAnalyses) { | |
| console.warn(`Cleaning up stuck analysis: ${analysisId} (running for ${Math.round((now - this.activeAnalyses.get(analysisId).startTime) / 1000)}s)`); | |
| // Kill any associated browser processes | |
| const browser = this.activeBrowsers.get(analysisId); | |
| if (browser) { | |
| try { | |
| await browser.close(); | |
| console.log(`Closed stuck browser for analysis ${analysisId}`); | |
| } catch (error) { | |
| console.error(`Error closing stuck browser for ${analysisId}:`, error.message); | |
| } | |
| this.activeBrowsers.delete(analysisId); | |
| } | |
| // Mark analysis as failed in database | |
| await this.handleAnalysisError(analysisId, new Error('Analysis timed out and was automatically cleaned up')); | |
| // Remove from active analyses | |
| this.activeAnalyses.delete(analysisId); | |
| } | |
| if (stuckAnalyses.length > 0) { | |
| console.log(`Cleaned up ${stuckAnalyses.length} stuck analyses`); | |
| } | |
| } | |
| registerBrowser(analysisId, browser) { | |
| this.activeBrowsers.set(analysisId, browser); | |
| } | |
| unregisterBrowser(analysisId) { | |
| this.activeBrowsers.delete(analysisId); | |
| } | |
| // Clean shutdown | |
| async shutdown() { | |
| if (this.cleanupInterval) { | |
| clearInterval(this.cleanupInterval); | |
| } | |
| // Close all active browsers | |
| for (const [analysisId, browser] of this.activeBrowsers.entries()) { | |
| try { | |
| await browser.close(); | |
| console.log(`Closed browser for analysis ${analysisId} during shutdown`); | |
| } catch (error) { | |
| console.error(`Error closing browser during shutdown:`, error.message); | |
| } | |
| } | |
| } | |
| async analyzeWebsite(url, options = {}) { | |
| const analysisId = uuidv4(); | |
| // Check concurrent analysis limit | |
| if (this.activeAnalyses.size >= this.maxConcurrentAnalyses) { | |
| throw new Error('Maximum concurrent analyses reached. Please try again later.'); | |
| } | |
| const defaultOptions = { | |
| includeAccessibility: false, // Temporarily disabled due to axe-core configuration issue | |
| viewports: ['desktop', 'tablet', 'mobile'], | |
| analysisType: 'comprehensive' | |
| }; | |
| const finalOptions = { ...defaultOptions, ...options }; | |
| try { | |
| // Initialize analysis record | |
| await this.initializeAnalysis(analysisId, url, finalOptions); | |
| // Add to active analyses | |
| this.activeAnalyses.set(analysisId, { | |
| url, | |
| startTime: Date.now(), | |
| status: 'processing' | |
| }); | |
| // Start analysis pipeline in background | |
| this.runAnalysisPipeline(analysisId, url, finalOptions) | |
| .catch(error => { | |
| console.error(`Analysis ${analysisId} failed:`, error); | |
| this.handleAnalysisError(analysisId, error); | |
| }) | |
| .finally(() => { | |
| this.activeAnalyses.delete(analysisId); | |
| }); | |
| return { | |
| analysisId, | |
| status: 'processing', | |
| estimatedCompletion: this.estimateCompletionTime(finalOptions), | |
| url | |
| }; | |
| } catch (error) { | |
| this.activeAnalyses.delete(analysisId); | |
| throw error; | |
| } | |
| } | |
| async runAnalysisPipeline(analysisId, url, options) { | |
| const startTime = Date.now(); | |
| try { | |
| // 1. Validate URL and capture screenshots (25% progress) | |
| await this.updateProgress(analysisId, 10, 'Validating URL'); | |
| await this.screenshotService.validateUrl(url); | |
| await this.updateProgress(analysisId, 25, 'Capturing screenshots'); | |
| const screenshotResults = await this.screenshotService.captureMultipleViewports(url, options.viewports, analysisId); | |
| if (screenshotResults.errors) { | |
| console.warn('Screenshot errors:', screenshotResults.errors); | |
| } | |
| await this.storeScreenshots(analysisId, screenshotResults.screenshots); | |
| // Screenshots completed - update progress | |
| await this.updateProgress(analysisId, 40, 'Screenshots completed'); | |
| // 2. Run visual design analysis (60% progress) | |
| await this.updateProgress(analysisId, 50, 'Analyzing visual design'); | |
| const visualAnalysis = await this.analyzeVisualDesign(screenshotResults.screenshots); | |
| await this.storeAnalysisResult(analysisId, 'visual_design', visualAnalysis); | |
| // 3. Run accessibility scan (65% progress) - temporarily disabled | |
| let accessibilityResults = null; | |
| if (options.includeAccessibility) { | |
| await this.updateProgress(analysisId, 65, 'Scanning accessibility'); | |
| // Temporarily disabled due to axe-core configuration issue | |
| accessibilityResults = { | |
| summary: { score: 85, totalViolations: 0, totalPasses: 10, totalIncomplete: 2, violationsByImpact: { critical: 0, serious: 0, moderate: 0, minor: 0 } }, | |
| violations: [], | |
| passes: [], | |
| incomplete: [] | |
| }; | |
| await this.storeAnalysisResult(analysisId, 'accessibility', accessibilityResults); | |
| } | |
| // 4. Generate AI critique (85% progress) | |
| await this.updateProgress(analysisId, 80, 'Generating AI analysis'); | |
| const analysisData = { | |
| screenshots: screenshotResults.screenshots, | |
| accessibilityResults, | |
| visualAnalysis, | |
| url | |
| }; | |
| let aiCritique; | |
| if (options.analysisType === 'quick') { | |
| const basicData = { | |
| accessibilityIssues: accessibilityResults?.summary?.totalViolations || 0, | |
| viewports: options.viewports | |
| }; | |
| aiCritique = await this.aiCritiqueService.generateQuickCritique(url, basicData); | |
| } else { | |
| aiCritique = await this.aiCritiqueService.generateUXCritique(analysisData); | |
| } | |
| await this.storeAnalysisResult(analysisId, 'ux_critique', aiCritique); | |
| // 4. Generate final report (100% progress) | |
| await this.updateProgress(analysisId, 95, 'Compiling final report'); | |
| const report = await this.compileReport({ | |
| screenshots: screenshotResults.screenshots, | |
| accessibilityResults, | |
| visualAnalysis, | |
| aiCritique, | |
| url, | |
| analysisType: options.analysisType | |
| }); | |
| await this.storeAnalysisResult(analysisId, 'final_report', report); | |
| // 5. Generate implementation code (if requested) | |
| if (options.includeCodeGeneration !== false && this.codeGenerationService) { | |
| await this.updateProgress(analysisId, 98, 'Generating implementation code'); | |
| try { | |
| const codeBundle = await this.codeGenerationService.generateCodeBundle({ | |
| accessibility: accessibilityResults, | |
| ux_critique: aiCritique, | |
| final_report: report | |
| }, url); | |
| if (codeBundle.success) { | |
| await this.storeAnalysisResult(analysisId, 'implementation_code', codeBundle.bundle); | |
| } | |
| } catch (codeError) { | |
| console.warn('Code generation failed, continuing without code:', codeError.message); | |
| } | |
| } | |
| // 6. Finalize analysis | |
| const duration = Date.now() - startTime; | |
| await this.finalizeAnalysis(analysisId, duration); | |
| // Store usage stats | |
| await this.storeUsageStats(analysisId, url, duration, options.viewports.length); | |
| } catch (error) { | |
| await this.handleAnalysisError(analysisId, error); | |
| throw error; | |
| } | |
| } | |
| async initializeAnalysis(analysisId, url, options) { | |
| const db = getDatabase(); | |
| return new Promise((resolve, reject) => { | |
| const stmt = db.prepare(` | |
| INSERT INTO analyses (id, url, status, progress, options, created_at) | |
| VALUES (?, ?, ?, ?, ?, datetime('now')) | |
| `); | |
| stmt.run([ | |
| analysisId, | |
| url, | |
| 'processing', | |
| 0, | |
| JSON.stringify(options) | |
| ], function(err) { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(); | |
| } | |
| }); | |
| stmt.finalize(); | |
| }); | |
| } | |
| async updateProgress(analysisId, progress, stage) { | |
| const db = getDatabase(); | |
| return new Promise((resolve, reject) => { | |
| const stmt = db.prepare(` | |
| UPDATE analyses | |
| SET progress = ?, stage = ? | |
| WHERE id = ? | |
| `); | |
| stmt.run([progress, stage, analysisId], function(err) { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(); | |
| } | |
| }); | |
| stmt.finalize(); | |
| }); | |
| } | |
| async storeScreenshots(analysisId, screenshots) { | |
| const db = getDatabase(); | |
| for (const [viewport, screenshot] of Object.entries(screenshots)) { | |
| await new Promise((resolve, reject) => { | |
| const stmt = db.prepare(` | |
| INSERT INTO screenshots (id, analysis_id, viewport, file_path, width, height, file_size) | |
| VALUES (?, ?, ?, ?, ?, ?, ?) | |
| `); | |
| stmt.run([ | |
| screenshot.id, | |
| analysisId, | |
| viewport, | |
| screenshot.filepath, | |
| screenshot.width, | |
| screenshot.height, | |
| screenshot.fileSize | |
| ], function(err) { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(); | |
| } | |
| }); | |
| stmt.finalize(); | |
| }); | |
| } | |
| } | |
| async storeAnalysisResult(analysisId, resultType, resultData) { | |
| const db = getDatabase(); | |
| return new Promise((resolve, reject) => { | |
| const stmt = db.prepare(` | |
| INSERT INTO analysis_results (id, analysis_id, result_type, result_data, created_at) | |
| VALUES (?, ?, ?, ?, datetime('now')) | |
| `); | |
| stmt.run([ | |
| uuidv4(), | |
| analysisId, | |
| resultType, | |
| JSON.stringify(resultData) | |
| ], function(err) { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(); | |
| } | |
| }); | |
| stmt.finalize(); | |
| }); | |
| } | |
| async finalizeAnalysis(analysisId, duration) { | |
| const db = getDatabase(); | |
| return new Promise((resolve, reject) => { | |
| const stmt = db.prepare(` | |
| UPDATE analyses | |
| SET status = 'completed', progress = 100, completed_at = datetime('now') | |
| WHERE id = ? | |
| `); | |
| stmt.run([analysisId], function(err) { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(); | |
| } | |
| }); | |
| stmt.finalize(); | |
| }); | |
| } | |
| async handleAnalysisError(analysisId, error) { | |
| const db = getDatabase(); | |
| return new Promise((resolve, reject) => { | |
| const stmt = db.prepare(` | |
| UPDATE analyses | |
| SET status = 'failed', error_message = ? | |
| WHERE id = ? | |
| `); | |
| stmt.run([error.message, analysisId], function(err) { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(); | |
| } | |
| }); | |
| stmt.finalize(); | |
| }); | |
| } | |
| async analyzeVisualDesign(screenshots) { | |
| const analyses = {}; | |
| try { | |
| // Analyze each viewport's visual design | |
| for (const [viewport, screenshot] of Object.entries(screenshots)) { | |
| console.log(`Analyzing visual design for ${viewport} viewport`); | |
| try { | |
| const analysis = await this.visualDesignAnalyzer.analyzeVisualDesign( | |
| screenshot.filepath, | |
| viewport | |
| ); | |
| analyses[viewport] = analysis; | |
| } catch (error) { | |
| console.error(`Visual design analysis failed for ${viewport}:`, error.message); | |
| analyses[viewport] = { | |
| viewport, | |
| error: error.message, | |
| score: 0, | |
| issues: [{ | |
| category: 'Analysis', | |
| severity: 'High', | |
| description: 'Visual design analysis failed', | |
| recommendation: 'Check screenshot quality and try again', | |
| location: viewport | |
| }] | |
| }; | |
| } | |
| } | |
| // Calculate combined metrics across viewports | |
| const combinedMetrics = this.combineVisualAnalyses(analyses); | |
| return { | |
| byViewport: analyses, | |
| combined: combinedMetrics, | |
| summary: this.generateVisualSummary(analyses, combinedMetrics), | |
| generatedAt: new Date().toISOString() | |
| }; | |
| } catch (error) { | |
| console.error('Visual design analysis error:', error); | |
| throw new Error(`Failed to analyze visual design: ${error.message}`); | |
| } | |
| } | |
| combineVisualAnalyses(analyses) { | |
| const viewports = Object.keys(analyses); | |
| const validAnalyses = viewports.filter(v => !analyses[v].error); | |
| if (validAnalyses.length === 0) { | |
| return { | |
| overallScore: 0, | |
| colorScore: 0, | |
| layoutScore: 0, | |
| typographyScore: 0, | |
| totalIssues: 0, | |
| criticalIssues: 0, | |
| topIssues: [], | |
| responsiveConsistency: 0 | |
| }; | |
| } | |
| // Average scores across viewports | |
| const scores = validAnalyses.map(v => analyses[v].score || 0); | |
| const overallScore = scores.reduce((sum, score) => sum + score, 0) / scores.length; | |
| // Collect all issues | |
| const allIssues = validAnalyses.flatMap(v => analyses[v].issues || []); | |
| const criticalIssues = allIssues.filter(issue => issue.severity === 'High').length; | |
| return { | |
| overallScore: Math.round(overallScore), | |
| totalIssues: allIssues.length, | |
| criticalIssues, | |
| topIssues: this.getTopIssues(allIssues), | |
| responsiveConsistency: this.calculateResponsiveConsistency(validAnalyses.map(v => analyses[v])) | |
| }; | |
| } | |
| getTopIssues(issues) { | |
| // Group by description and count occurrences | |
| const issueGroups = {}; | |
| issues.forEach(issue => { | |
| const key = `${issue.category}:${issue.description}`; | |
| if (!issueGroups[key]) { | |
| issueGroups[key] = { ...issue, count: 0, viewports: [] }; | |
| } | |
| issueGroups[key].count++; | |
| issueGroups[key].viewports.push(issue.location); | |
| }); | |
| // Return top 5 most common issues | |
| return Object.values(issueGroups) | |
| .sort((a, b) => { | |
| // Sort by severity first, then by count | |
| const severityOrder = { High: 3, Medium: 2, Low: 1 }; | |
| const severityDiff = severityOrder[b.severity] - severityOrder[a.severity]; | |
| return severityDiff || b.count - a.count; | |
| }) | |
| .slice(0, 5) | |
| .map(issue => ({ | |
| category: issue.category, | |
| severity: issue.severity, | |
| description: issue.description, | |
| recommendation: issue.recommendation, | |
| affectedViewports: [...new Set(issue.viewports)], | |
| frequency: issue.count | |
| })); | |
| } | |
| calculateResponsiveConsistency(analyses) { | |
| if (analyses.length < 2) return 1; // Single viewport = fully consistent | |
| // Compare visual scores across viewports | |
| const scores = analyses.map(a => a.score || 0); | |
| const average = scores.reduce((sum, score) => sum + score, 0) / scores.length; | |
| const variance = scores.reduce((sum, score) => sum + Math.pow(score - average, 2), 0) / scores.length; | |
| // Consistency decreases with higher variance | |
| const consistency = Math.max(0, 1 - (Math.sqrt(variance) / 100)); | |
| return Math.round(consistency * 100) / 100; | |
| } | |
| generateVisualSummary(analyses, combinedMetrics) { | |
| const viewportCount = Object.keys(analyses).length; | |
| const successfulAnalyses = Object.values(analyses).filter(a => !a.error).length; | |
| let grade = 'Unknown'; | |
| if (combinedMetrics.overallScore >= 90) grade = 'Excellent'; | |
| else if (combinedMetrics.overallScore >= 75) grade = 'Good'; | |
| else if (combinedMetrics.overallScore >= 60) grade = 'Fair'; | |
| else if (combinedMetrics.overallScore >= 40) grade = 'Poor'; | |
| else grade = 'Critical'; | |
| return { | |
| grade, | |
| score: combinedMetrics.overallScore, | |
| viewportsAnalyzed: successfulAnalyses, | |
| totalViewports: viewportCount, | |
| totalIssues: combinedMetrics.totalIssues, | |
| criticalIssues: combinedMetrics.criticalIssues, | |
| responsiveConsistency: combinedMetrics.responsiveConsistency, | |
| mainStrengths: this.identifyStrengths(analyses), | |
| mainWeaknesses: this.identifyWeaknesses(combinedMetrics.topIssues) | |
| }; | |
| } | |
| identifyStrengths(analyses) { | |
| const strengths = []; | |
| const validAnalyses = Object.values(analyses).filter(a => !a.error); | |
| if (validAnalyses.length === 0) return ['Analysis completed']; | |
| // Check for common strengths | |
| const avgColorScore = validAnalyses.reduce((sum, a) => sum + (a.colorAnalysis?.accessibility === 'excellent' ? 1 : 0), 0) / validAnalyses.length; | |
| const avgLayoutScore = validAnalyses.reduce((sum, a) => sum + (a.layoutAnalysis?.assessment === 'excellent' ? 1 : 0), 0) / validAnalyses.length; | |
| if (avgColorScore > 0.5) strengths.push('Excellent color accessibility'); | |
| if (avgLayoutScore > 0.5) strengths.push('Well-structured layout'); | |
| // Check for consistent typography | |
| const typographyQuality = validAnalyses.filter(a => a.typographyAnalysis?.hierarchy === 'good').length; | |
| if (typographyQuality / validAnalyses.length > 0.5) { | |
| strengths.push('Clear typographic hierarchy'); | |
| } | |
| return strengths.length > 0 ? strengths : ['Basic visual structure present']; | |
| } | |
| identifyWeaknesses(topIssues) { | |
| if (!topIssues || !Array.isArray(topIssues)) return []; | |
| return topIssues.slice(0, 3).map(issue => issue.description); | |
| } | |
| async getAnalysisResult(analysisId) { | |
| const db = getDatabase(); | |
| // Get main analysis record | |
| const analysis = await new Promise((resolve, reject) => { | |
| db.get( | |
| 'SELECT * FROM analyses WHERE id = ?', | |
| [analysisId], | |
| (err, row) => { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(row); | |
| } | |
| } | |
| ); | |
| }); | |
| if (!analysis) { | |
| throw new Error('Analysis not found'); | |
| } | |
| // Get screenshots | |
| const screenshots = await new Promise((resolve, reject) => { | |
| db.all( | |
| 'SELECT * FROM screenshots WHERE analysis_id = ?', | |
| [analysisId], | |
| (err, rows) => { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(rows); | |
| } | |
| } | |
| ); | |
| }); | |
| // Get analysis results | |
| const results = await new Promise((resolve, reject) => { | |
| db.all( | |
| 'SELECT * FROM analysis_results WHERE analysis_id = ?', | |
| [analysisId], | |
| (err, rows) => { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(rows); | |
| } | |
| } | |
| ); | |
| }); | |
| // Parse and organize results | |
| const organizedResults = {}; | |
| results.forEach(result => { | |
| organizedResults[result.result_type] = JSON.parse(result.result_data); | |
| }); | |
| return { | |
| id: analysis.id, | |
| url: analysis.url, | |
| status: analysis.status, | |
| progress: analysis.progress, | |
| options: JSON.parse(analysis.options || '{}'), | |
| createdAt: analysis.created_at, | |
| completedAt: analysis.completed_at, | |
| errorMessage: analysis.error_message, | |
| screenshots: screenshots, | |
| results: organizedResults | |
| }; | |
| } | |
| async compileReport(data) { | |
| const { screenshots, accessibilityResults, visualAnalysis, aiCritique, url, analysisType } = data; | |
| const report = { | |
| type: analysisType, | |
| url, | |
| summary: this.generateReportSummary(data), | |
| screenshots: Object.keys(screenshots), | |
| accessibility: accessibilityResults ? { | |
| score: accessibilityResults.summary.score, | |
| totalViolations: accessibilityResults.summary.totalViolations, | |
| topIssues: accessibilityResults.violations.slice(0, 5).map(v => ({ | |
| description: v.description, | |
| impact: v.impact, | |
| count: v.nodes.length | |
| })) | |
| } : null, | |
| visualDesign: visualAnalysis ? { | |
| overallScore: visualAnalysis.combined.overallScore, | |
| totalIssues: visualAnalysis.combined.totalIssues, | |
| criticalIssues: visualAnalysis.combined.criticalIssues, | |
| responsiveConsistency: visualAnalysis.combined.responsiveConsistency, | |
| topIssues: visualAnalysis.combined.topIssues?.slice(0, 3) || [], | |
| mainStrengths: visualAnalysis.summary.mainStrengths, | |
| mainWeaknesses: visualAnalysis.summary.mainWeaknesses | |
| } : null, | |
| ux: aiCritique.structured_critique ? { | |
| overallScore: aiCritique.structured_critique.overall_assessment.score, | |
| topRecommendations: aiCritique.structured_critique.recommendations.slice(0, 3), | |
| keyStrengths: aiCritique.structured_critique.overall_assessment.strengths, | |
| keyWeaknesses: aiCritique.structured_critique.overall_assessment.weaknesses | |
| } : { | |
| quickCritique: aiCritique.quick_critique | |
| }, | |
| generatedAt: new Date().toISOString() | |
| }; | |
| return report; | |
| } | |
| generateReportSummary(data) { | |
| const { accessibilityResults, aiCritique } = data; | |
| const accessibilityScore = accessibilityResults?.summary?.score || 0; | |
| const uxScore = aiCritique.structured_critique?.overall_assessment?.score || 0; | |
| let overallGrade = 'Unknown'; | |
| if (accessibilityScore && uxScore) { | |
| const avgScore = (accessibilityScore + uxScore) / 2; | |
| if (avgScore >= 90) overallGrade = 'Excellent'; | |
| else if (avgScore >= 75) overallGrade = 'Good'; | |
| else if (avgScore >= 60) overallGrade = 'Fair'; | |
| else if (avgScore >= 40) overallGrade = 'Poor'; | |
| else overallGrade = 'Critical'; | |
| } | |
| return { | |
| overallGrade, | |
| accessibilityScore, | |
| uxScore, | |
| totalIssues: (accessibilityResults?.summary?.totalViolations || 0), | |
| priorityLevel: this.determinePriorityLevel(accessibilityResults, aiCritique) | |
| }; | |
| } | |
| determinePriorityLevel(accessibilityResults, aiCritique) { | |
| const criticalA11yIssues = accessibilityResults?.summary?.violationsByImpact?.critical || 0; | |
| const highPriorityUX = aiCritique.structured_critique?.recommendations?.filter(r => r.priority === 'High').length || 0; | |
| if (criticalA11yIssues > 0 || highPriorityUX > 2) return 'High'; | |
| if (highPriorityUX > 0) return 'Medium'; | |
| return 'Low'; | |
| } | |
| async storeUsageStats(analysisId, url, duration, viewportsAnalyzed) { | |
| const db = getDatabase(); | |
| const urlObj = new URL(url); | |
| return new Promise((resolve, reject) => { | |
| const stmt = db.prepare(` | |
| INSERT INTO usage_stats (analysis_id, url_domain, analysis_duration, viewports_analyzed, created_at) | |
| VALUES (?, ?, ?, ?, datetime('now')) | |
| `); | |
| stmt.run([ | |
| analysisId, | |
| urlObj.hostname, | |
| duration, | |
| viewportsAnalyzed | |
| ], function(err) { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(); | |
| } | |
| }); | |
| stmt.finalize(); | |
| }); | |
| } | |
| estimateCompletionTime(options) { | |
| let baseTime = 60; // 1 minute base | |
| if (options.includeAccessibility) baseTime += 30; | |
| if (options.analysisType === 'comprehensive') baseTime += 60; | |
| baseTime += (options.viewports.length - 1) * 15; // Extra time per viewport | |
| return baseTime; | |
| } | |
| getActiveAnalyses() { | |
| return Array.from(this.activeAnalyses.entries()).map(([id, data]) => ({ | |
| id, | |
| ...data, | |
| duration: Date.now() - data.startTime | |
| })); | |
| } | |
| } | |
| module.exports = AnalysisService; |