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:
2026-01-14 00:38:40 +01:00
parent ca21b9538d
commit a7f8301196
73 changed files with 12878 additions and 2003 deletions

View File

@@ -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) {