Add database adapter system, production deployment configs, and new dashboard components
- Add PostgreSQL and SQLite database adapters with factory pattern - Add migration script for SQLite to PostgreSQL - Add production Dockerfiles and docker-compose configs - Add deployment documentation and scripts - Add BIA sync dashboard and matching service - Add data completeness configuration and components - Add new dashboard components (BusinessImportanceComparison, ComplexityDynamics, etc.) - Update various services and routes - Remove deprecated management-parameters.json and taxonomy files
This commit is contained in:
@@ -4,6 +4,8 @@ import { databaseService } from '../services/database.js';
|
||||
import { cmdbService } from '../services/cmdbService.js';
|
||||
import { logger } from '../services/logger.js';
|
||||
import { calculateRequiredEffortApplicationManagementWithBreakdown } from '../services/effortCalculation.js';
|
||||
import { findBIAMatch, loadBIAData, clearBIACache, calculateSimilarity } from '../services/biaMatchingService.js';
|
||||
import { calculateApplicationCompleteness } from '../services/dataCompletenessConfig.js';
|
||||
import type { SearchFilters, ReferenceValue, ClassificationResult, ApplicationDetails, ApplicationStatus } from '../types/index.js';
|
||||
import type { Server, Flows, Certificate, Domain, AzureSubscription, CMDBObjectTypeName } from '../generated/jira-types.js';
|
||||
|
||||
@@ -51,6 +53,270 @@ router.get('/team-dashboard', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get team portfolio health metrics
|
||||
router.get('/team-portfolio-health', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const excludedStatusesParam = req.query.excludedStatuses as string | undefined;
|
||||
let excludedStatuses: ApplicationStatus[] = [];
|
||||
|
||||
if (excludedStatusesParam && excludedStatusesParam.trim().length > 0) {
|
||||
// Parse comma-separated statuses
|
||||
excludedStatuses = excludedStatusesParam
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0) as ApplicationStatus[];
|
||||
} else {
|
||||
// Default to excluding 'Closed' and 'Deprecated' if not specified
|
||||
excludedStatuses = ['Closed', 'Deprecated'];
|
||||
}
|
||||
|
||||
const data = await dataService.getTeamPortfolioHealth(excludedStatuses);
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
logger.error('Failed to get team portfolio health', error);
|
||||
res.status(500).json({ error: 'Failed to get team portfolio health' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get Business Importance vs BIA comparison
|
||||
// NOTE: This must come BEFORE the /:id route to avoid route conflicts
|
||||
router.get('/business-importance-comparison', async (req: Request, res: Response) => {
|
||||
try {
|
||||
logger.info('Business Importance comparison endpoint called');
|
||||
const data = await dataService.getBusinessImportanceComparison();
|
||||
logger.info(`Business Importance comparison: returning ${data.applications.length} applications`);
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
logger.error('Failed to get business importance comparison', error);
|
||||
res.status(500).json({ error: 'Failed to get business importance comparison' });
|
||||
}
|
||||
});
|
||||
|
||||
// Test BIA data loading (for debugging)
|
||||
router.get('/bia-test', async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (req.query.clear === 'true') {
|
||||
clearBIACache();
|
||||
}
|
||||
const biaData = loadBIAData();
|
||||
res.json({
|
||||
recordCount: biaData.length,
|
||||
records: biaData.slice(0, 20), // First 20 records
|
||||
sample: biaData.slice(0, 5).map(r => `${r.applicationName} -> ${r.biaValue}`),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to test BIA data loading', error);
|
||||
res.status(500).json({ error: 'Failed to test BIA data loading', details: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// Comprehensive debug endpoint for BIA matching
|
||||
router.get('/bia-debug', async (req: Request, res: Response) => {
|
||||
try {
|
||||
clearBIACache();
|
||||
const biaData = loadBIAData();
|
||||
|
||||
// Get a few sample applications
|
||||
const searchResult = await dataService.searchApplications({}, 1, 50);
|
||||
const sampleApps = searchResult.applications.slice(0, 10);
|
||||
|
||||
// Test specific examples mentioned by user
|
||||
const testNames = ['Aanmeldzuilen', 'PregnaOne', 'BeagleBoxx'];
|
||||
const testApps = searchResult.applications.filter(app =>
|
||||
testNames.some(name =>
|
||||
app.name.toLowerCase().includes(name.toLowerCase()) ||
|
||||
(app.searchReference && app.searchReference.toLowerCase().includes(name.toLowerCase()))
|
||||
)
|
||||
);
|
||||
|
||||
const debugResults = [];
|
||||
|
||||
// Test each sample app
|
||||
for (const app of [...sampleApps, ...testApps]) {
|
||||
const matchResult = findBIAMatch(app.name, app.searchReference);
|
||||
|
||||
// Find all potential matches in Excel data for detailed analysis
|
||||
const normalizedAppName = app.name.toLowerCase().trim();
|
||||
const normalizedSearchRef = app.searchReference ? app.searchReference.toLowerCase().trim() : null;
|
||||
|
||||
const potentialMatches = biaData.map(record => {
|
||||
const normalizedRecordName = record.applicationName.toLowerCase().trim();
|
||||
const exactNameMatch = normalizedAppName === normalizedRecordName;
|
||||
const exactSearchRefMatch = normalizedSearchRef === normalizedRecordName;
|
||||
const startsWithApp = normalizedRecordName.startsWith(normalizedAppName) || normalizedAppName.startsWith(normalizedRecordName);
|
||||
const containsApp = normalizedRecordName.includes(normalizedAppName) || normalizedAppName.includes(normalizedRecordName);
|
||||
|
||||
// Calculate similarity using the exported function
|
||||
const similarity = calculateSimilarity(normalizedAppName, normalizedRecordName);
|
||||
|
||||
return {
|
||||
excelName: record.applicationName,
|
||||
excelBIA: record.biaValue,
|
||||
exactNameMatch,
|
||||
exactSearchRefMatch,
|
||||
startsWithApp,
|
||||
containsApp,
|
||||
similarity: similarity,
|
||||
};
|
||||
}).filter(m => m.exactNameMatch || m.exactSearchRefMatch || m.startsWithApp || m.containsApp || m.similarity > 0.5);
|
||||
|
||||
debugResults.push({
|
||||
appName: app.name,
|
||||
searchReference: app.searchReference,
|
||||
matchResult: matchResult,
|
||||
potentialMatches: potentialMatches.slice(0, 5), // Top 5 potential matches
|
||||
allExcelRecords: biaData.length,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
excelDataLoaded: biaData.length,
|
||||
sampleExcelRecords: biaData.slice(0, 20),
|
||||
debugResults,
|
||||
testNames: testNames.map(name => {
|
||||
const matchingExcel = biaData.filter(r =>
|
||||
r.applicationName.toLowerCase().includes(name.toLowerCase()) ||
|
||||
name.toLowerCase().includes(r.applicationName.toLowerCase())
|
||||
);
|
||||
return {
|
||||
name,
|
||||
matchingExcelRecords: matchingExcel,
|
||||
};
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to debug BIA matching', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to debug BIA matching',
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get BIA comparison data
|
||||
router.get('/bia-comparison', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// Clear cache and reload BIA data to ensure fresh data
|
||||
const { loadBIAData, clearBIACache } = await import('../services/biaMatchingService.js');
|
||||
clearBIACache();
|
||||
|
||||
// Load fresh data
|
||||
const testBIAData = loadBIAData();
|
||||
logger.info(`BIA comparison: Loaded ${testBIAData.length} records from Excel file`);
|
||||
if (testBIAData.length === 0) {
|
||||
logger.error('BIA comparison: No Excel data loaded - check if BIA.xlsx exists and is readable');
|
||||
logger.error('This could indicate:');
|
||||
logger.error(' 1. Excel file not found at backend/data/BIA.xlsx');
|
||||
logger.error(' 2. Column C (index 2) or K (index 10) not found');
|
||||
logger.error(' 3. No valid BIA values (A-F) in column K');
|
||||
} else {
|
||||
logger.info(`BIA comparison: Sample Excel records: ${testBIAData.slice(0, 5).map(r => `"${r.applicationName}"->${r.biaValue}`).join(', ')}`);
|
||||
}
|
||||
|
||||
// Get all applications (no filters)
|
||||
const searchResult = await dataService.searchApplications({}, 1, 10000);
|
||||
logger.info(`BIA comparison: Found ${searchResult.applications.length} applications to compare`);
|
||||
|
||||
// Get all Business Impact Analyses for mapping
|
||||
const biaReferences = await dataService.getBusinessImpactAnalyses();
|
||||
const biaMap = new Map<string, ReferenceValue>();
|
||||
biaReferences.forEach(bia => {
|
||||
// Map by the letter (A-F) - typically the name starts with the letter
|
||||
const letter = bia.name.charAt(0).toUpperCase();
|
||||
if (/^[A-F]$/.test(letter)) {
|
||||
biaMap.set(letter, bia);
|
||||
}
|
||||
// Also try to match by key or name containing the letter
|
||||
if (bia.name.toUpperCase().includes(' - ')) {
|
||||
const parts = bia.name.split(' - ');
|
||||
if (parts.length > 0 && /^[A-F]$/.test(parts[0].trim().toUpperCase())) {
|
||||
biaMap.set(parts[0].trim().toUpperCase(), bia);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const applications = searchResult.applications;
|
||||
const comparisonItems = [];
|
||||
|
||||
let matched = 0;
|
||||
let mismatched = 0;
|
||||
let notFound = 0;
|
||||
let noExcelBIA = 0;
|
||||
|
||||
for (const app of applications) {
|
||||
// Find BIA match in Excel
|
||||
const matchResult = findBIAMatch(app.name, app.searchReference);
|
||||
|
||||
// Log first few matches for debugging
|
||||
if (comparisonItems.length < 5) {
|
||||
logger.debug(`BIA match for "${app.name}": ${matchResult.biaValue || 'NOT FOUND'} (type: ${matchResult.matchType || 'none'})`);
|
||||
}
|
||||
|
||||
// Extract BIA letter from current BIA name (handle formats like "A", "A - Test/Archief", etc.)
|
||||
let currentBIALetter: string | null = null;
|
||||
if (app.businessImpactAnalyse?.name) {
|
||||
const match = app.businessImpactAnalyse.name.match(/^([A-F])/);
|
||||
if (match) {
|
||||
currentBIALetter = match[1].toUpperCase();
|
||||
} else {
|
||||
// Fallback to first character
|
||||
currentBIALetter = app.businessImpactAnalyse.name.charAt(0).toUpperCase();
|
||||
if (!/^[A-F]$/.test(currentBIALetter)) {
|
||||
currentBIALetter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const excelBIALetter = matchResult.biaValue;
|
||||
|
||||
let matchStatus: 'match' | 'mismatch' | 'not_found' | 'no_excel_bia';
|
||||
|
||||
if (!excelBIALetter) {
|
||||
matchStatus = 'no_excel_bia';
|
||||
noExcelBIA++;
|
||||
} else if (!currentBIALetter) {
|
||||
matchStatus = 'not_found';
|
||||
notFound++;
|
||||
} else if (currentBIALetter === excelBIALetter) {
|
||||
matchStatus = 'match';
|
||||
matched++;
|
||||
} else {
|
||||
matchStatus = 'mismatch';
|
||||
mismatched++;
|
||||
}
|
||||
|
||||
comparisonItems.push({
|
||||
id: app.id,
|
||||
key: app.key,
|
||||
name: app.name,
|
||||
searchReference: app.searchReference,
|
||||
currentBIA: app.businessImpactAnalyse,
|
||||
excelBIA: excelBIALetter,
|
||||
excelApplicationName: matchResult.excelApplicationName,
|
||||
matchStatus,
|
||||
matchType: matchResult.matchType,
|
||||
matchConfidence: matchResult.matchConfidence,
|
||||
allMatches: matchResult.allMatches,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
applications: comparisonItems,
|
||||
summary: {
|
||||
total: applications.length,
|
||||
matched,
|
||||
mismatched,
|
||||
notFound,
|
||||
noExcelBIA,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get BIA comparison', error);
|
||||
res.status(500).json({ error: 'Failed to get BIA comparison' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get application by ID
|
||||
// Query params:
|
||||
// - mode=edit: Force refresh from Jira for editing (includes _jiraUpdatedAt for conflict detection)
|
||||
@@ -60,7 +326,7 @@ router.get('/:id', async (req: Request, res: Response) => {
|
||||
const mode = req.query.mode as string | undefined;
|
||||
|
||||
// Don't treat special routes as application IDs
|
||||
if (id === 'team-dashboard' || id === 'calculate-effort' || id === 'search') {
|
||||
if (id === 'team-dashboard' || id === 'team-portfolio-health' || id === 'business-importance-comparison' || id === 'bia-comparison' || id === 'bia-test' || id === 'calculate-effort' || id === 'search') {
|
||||
res.status(404).json({ error: 'Route not found' });
|
||||
return;
|
||||
}
|
||||
@@ -75,7 +341,14 @@ router.get('/:id', async (req: Request, res: Response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(application);
|
||||
// Calculate data completeness percentage
|
||||
const completenessPercentage = calculateApplicationCompleteness(application);
|
||||
const applicationWithCompleteness = {
|
||||
...application,
|
||||
dataCompletenessPercentage: Math.round(completenessPercentage * 10) / 10, // Round to 1 decimal
|
||||
};
|
||||
|
||||
res.json(applicationWithCompleteness);
|
||||
} catch (error) {
|
||||
logger.error('Failed to get application', error);
|
||||
res.status(500).json({ error: 'Failed to get application' });
|
||||
@@ -183,7 +456,7 @@ router.put('/:id', async (req: Request, res: Response) => {
|
||||
source: actualUpdates.source || 'MANUAL',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
databaseService.saveClassificationResult(classificationResult);
|
||||
await databaseService.saveClassificationResult(classificationResult);
|
||||
|
||||
// Return updated application
|
||||
const updatedApp = result.data || await dataService.getApplicationById(id);
|
||||
@@ -279,7 +552,7 @@ router.post('/calculate-effort', async (req: Request, res: Response) => {
|
||||
router.get('/:id/history', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const history = databaseService.getClassificationsByApplicationId(id);
|
||||
const history = await databaseService.getClassificationsByApplicationId(id);
|
||||
res.json(history);
|
||||
} catch (error) {
|
||||
logger.error('Failed to get classification history', error);
|
||||
@@ -352,7 +625,7 @@ router.get('/:id/related/:objectType', async (req: Request, res: Response) => {
|
||||
const objects = relatedObjects.map(obj => {
|
||||
// Extract attributes from the object
|
||||
const attributes: Record<string, string | null> = {};
|
||||
const objData = obj as Record<string, unknown>;
|
||||
const objData = obj as unknown as Record<string, unknown>;
|
||||
|
||||
// If specific attributes are requested, extract those
|
||||
if (requestedAttrs.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user