UNPKG

@digitalnodecom/node-red-contrib-analyzer

Version:

A Node-RED global service that monitors function nodes for debugging artifacts and performance issues. Features real-time quality metrics, Vue.js dashboard, and comprehensive code analysis.

328 lines (296 loc) 12.4 kB
const { detectDebuggingTraits } = require('../../detection/detector'); const QualityMetrics = require('../../detection/quality-metrics'); const { getDatabase } = require('../../database/db'); const qualityMetrics = new QualityMetrics(); // Get overall quality summary async function getQualitySummary(req, res) { try { const db = req.db || getDatabase(); // Get latest quality metrics for all flows const flowMetrics = await db.all(` SELECT flow_id, flow_name, total_issues, nodes_with_issues, nodes_with_critical_issues, total_function_nodes, quality_score, complexity_score, created_at FROM code_quality_metrics WHERE id IN ( SELECT MAX(id) FROM code_quality_metrics GROUP BY flow_id ) ORDER BY created_at DESC `); // Calculate overall metrics let totalIssues = 0; let totalNodes = 0; let totalNodesWithIssues = 0; let totalCriticalIssues = 0; let weightedQualityScore = 0; let totalWeight = 0; flowMetrics.forEach(flow => { totalIssues += flow.total_issues; totalNodes += flow.total_function_nodes; totalNodesWithIssues += flow.nodes_with_issues; totalCriticalIssues += flow.nodes_with_critical_issues; const weight = flow.total_function_nodes; weightedQualityScore += flow.quality_score * weight; totalWeight += weight; }); const overallQualityScore = totalWeight > 0 ? weightedQualityScore / totalWeight : 100; const qualityGrade = qualityMetrics.getQualityGrade(overallQualityScore); const technicalDebtRatio = totalNodes > 0 ? (totalIssues / totalNodes) : 0; res.json({ summary: { averageQualityScore: Math.round(overallQualityScore * 100) / 100, totalIssues, totalFlows: flowMetrics.length, totalFunctionNodes: totalNodes, qualityGrade, nodesWithIssues: totalNodesWithIssues, criticalIssues: totalCriticalIssues, technicalDebtRatio: Math.round(technicalDebtRatio * 1000) / 1000 }, flows: flowMetrics.map(flow => ({ flow_id: flow.flow_id, flow_name: flow.flow_name, quality_score: Math.round(flow.quality_score * 100) / 100, qualityGrade: qualityMetrics.getQualityGrade(flow.quality_score), total_issues: flow.total_issues, nodes_with_issues: flow.nodes_with_issues, nodes_with_critical_issues: flow.nodes_with_critical_issues, total_function_nodes: flow.total_function_nodes, created_at: flow.created_at })) }); } catch (error) { console.error('Error getting quality summary:', error); res.status(500).json({ error: 'Failed to get quality summary' }); } } // Get quality history for trends async function getQualityHistory(req, res) { try { const db = req.db || getDatabase(); const hours = parseInt(req.query.hours) || 24; const cutoffTime = new Date(Date.now() - (hours * 60 * 60 * 1000)).toISOString(); const history = await db.all(` SELECT strftime('%Y-%m-%d %H:00:00', created_at) as hour, AVG(quality_score) as avg_quality_score, SUM(total_issues) as total_issues, SUM(total_function_nodes) as total_nodes, COUNT(DISTINCT flow_id) as active_flows FROM code_quality_metrics WHERE created_at >= ? GROUP BY strftime('%Y-%m-%d %H:00:00', created_at) ORDER BY hour ASC `, [cutoffTime]); res.json({ timeRange: `${hours} hours`, history: (history || []).map(point => ({ id: `${point.hour}`, created_at: point.hour, quality_score: Math.round((point.avg_quality_score || 0) * 100) / 100, total_issues: point.total_issues || 0, total_nodes: point.total_nodes || 0, active_flows: point.active_flows || 0 })) }); } catch (error) { console.error('Error getting quality history:', error); res.status(500).json({ error: 'Failed to get quality history' }); } } // Get quality details for a specific flow async function getFlowQuality(req, res) { try { const { flowId } = req.params; const db = req.db || getDatabase(); // Get latest quality metrics for the flow const flowMetrics = await db.get(` SELECT * FROM code_quality_metrics WHERE flow_id = ? ORDER BY created_at DESC LIMIT 1 `, [flowId]); if (!flowMetrics) { return res.status(404).json({ error: 'Flow not found or no quality data available' }); } // Parse issue types let issueTypes = {}; try { issueTypes = JSON.parse(flowMetrics.issue_types || '{}'); } catch (e) { console.error('Error parsing issue types:', e); } res.json({ flowId: flowMetrics.flow_id, flowName: flowMetrics.flow_name, qualityScore: Math.round(flowMetrics.quality_score * 100) / 100, qualityGrade: qualityMetrics.getQualityGrade(flowMetrics.quality_score), complexityScore: Math.round(flowMetrics.complexity_score * 100) / 100, totalIssues: flowMetrics.total_issues, nodesWithIssues: flowMetrics.nodes_with_issues, criticalIssues: flowMetrics.nodes_with_critical_issues, totalNodes: flowMetrics.total_function_nodes, issueTypes, lastUpdated: flowMetrics.created_at }); } catch (error) { console.error('Error getting flow quality:', error); res.status(500).json({ error: 'Failed to get flow quality data' }); } } // Get quality details for a specific node async function getNodeQuality(req, res) { try { const { flowId, nodeId } = req.params; const RED = req.RED; // RED instance should be injected via middleware if (!RED) { return res.status(500).json({ error: 'Node-RED instance not available' }); } // Find the node let nodeConfig = null; try { RED.nodes.eachNode(function(n) { if (n.id === nodeId && n.z === flowId) { nodeConfig = n; } }); } catch (err) { console.error('Error accessing Node-RED nodes:', err); return res.status(500).json({ error: 'Failed to access Node-RED nodes' }); } if (!nodeConfig || nodeConfig.type !== 'function') { return res.status(404).json({ error: 'Function node not found' }); } // Analyze the node's code const issues = detectDebuggingTraits(nodeConfig.func || '', 3); const linesOfCode = (nodeConfig.func || '').split('\n').length; const complexityScore = qualityMetrics.calculateComplexityScore(nodeConfig.func || ''); const nodeQualityScore = qualityMetrics.calculateNodeQualityScore(issues, linesOfCode); // Categorize issues by severity const issuesBySeverity = { critical: issues.filter(issue => ['top-level-return', 'debugger-statement'].includes(issue.type)), warning: issues.filter(issue => ['console-log', 'node-warn', 'todo-comment'].includes(issue.type)), info: issues.filter(issue => ['unused-variable', 'hardcoded-test', 'multiple-empty-lines'].includes(issue.type)) }; res.json({ nodeId, nodeName: nodeConfig.name || `Function Node ${nodeId.substring(0, 8)}`, flowId, qualityScore: Math.round(nodeQualityScore * 100) / 100, qualityGrade: qualityMetrics.getQualityGrade(nodeQualityScore), complexityScore: Math.round(complexityScore * 100) / 100, linesOfCode, totalIssues: issues.length, issues: issues.map(issue => ({ ...issue, severity: qualityMetrics.getIssueSeverity(issue.type) })), issuesBySeverity: { critical: issuesBySeverity.critical.length, warning: issuesBySeverity.warning.length, info: issuesBySeverity.info.length } }); } catch (error) { console.error('Error getting node quality:', error); res.status(500).json({ error: 'Failed to get node quality data' }); } } // Get detailed issues for a specific flow async function getFlowIssues(req, res) { try { const { flowId } = req.params; const db = req.db || getDatabase(); // Get latest flow metrics const flowMetrics = await db.get(` SELECT * FROM code_quality_metrics WHERE flow_id = ? ORDER BY created_at DESC LIMIT 1 `, [flowId]); if (!flowMetrics) { return res.status(404).json({ error: 'Flow not found or no quality data available' }); } // Get detailed node issues for this flow (latest entry per node) const nodeIssues = await db.all(` SELECT node_id, node_name, issues_count, issue_details, quality_score, complexity_score, lines_of_code FROM node_quality_metrics WHERE flow_id = ? AND id IN ( SELECT MAX(id) FROM node_quality_metrics WHERE flow_id = ? GROUP BY node_id ) ORDER BY issues_count DESC, quality_score ASC `, [flowId, flowId]); // Parse issue details for each node const detailedIssues = nodeIssues.map(node => { let issues = []; try { issues = JSON.parse(node.issue_details || '[]'); } catch (e) { console.error('Error parsing issue details for node:', node.node_id, e); } return { nodeId: node.node_id, nodeName: node.node_name || `Node ${node.node_id.substring(0, 8)}`, issuesCount: node.issues_count, qualityScore: Math.round(node.quality_score * 100) / 100, complexityScore: Math.round(node.complexity_score * 100) / 100, linesOfCode: node.lines_of_code, issues: issues.map(issue => ({ type: issue.type, message: issue.message, line: issue.line, severity: qualityMetrics.getIssueSeverity ? qualityMetrics.getIssueSeverity(issue.type) : 'info' })) }; }); // Parse issue types from flow metrics let issueTypesSummary = {}; try { issueTypesSummary = JSON.parse(flowMetrics.issue_types || '{}'); } catch (e) { console.error('Error parsing flow issue types:', e); } res.json({ flowId: flowMetrics.flow_id, flowName: flowMetrics.flow_name, qualityScore: Math.round(flowMetrics.quality_score * 100) / 100, totalIssues: flowMetrics.total_issues, nodesWithIssues: flowMetrics.nodes_with_issues, criticalIssues: flowMetrics.nodes_with_critical_issues, totalNodes: flowMetrics.total_function_nodes, issueTypesSummary, lastUpdated: flowMetrics.created_at, nodeDetails: detailedIssues }); } catch (error) { console.error('Error getting flow issues:', error); res.status(500).json({ error: 'Failed to get flow issues' }); } } module.exports = { getQualitySummary, getQualityHistory, getFlowQuality, getNodeQuality, getFlowIssues };