UI styling improvements: dashboard headers and navigation

- Restore blue PageHeader on Dashboard (/app-components)
- Update homepage (/) with subtle header design without blue bar
- Add uniform PageHeader styling to application edit page
- Fix Rapporten link on homepage to point to /reports overview
- Improve header descriptions spacing for better readability
This commit is contained in:
2026-01-21 03:24:56 +01:00
parent e276e77fbc
commit cdee0e8819
138 changed files with 24551 additions and 3352 deletions

View File

@@ -326,8 +326,9 @@ router.get('/bia-comparison', async (req: Request, res: Response) => {
// Query params:
// - mode=edit: Force refresh from Jira for editing (includes _jiraUpdatedAt for conflict detection)
router.get('/:id', async (req: Request, res: Response) => {
const id = getParamString(req, 'id');
try {
const id = getParamString(req, 'id');
const mode = getQueryString(req, 'mode');
// Don't treat special routes as application IDs
@@ -342,7 +343,7 @@ router.get('/:id', async (req: Request, res: Response) => {
: await dataService.getApplicationById(id);
if (!application) {
res.status(404).json({ error: 'Application not found' });
res.status(404).json({ error: 'Application not found', id });
return;
}
@@ -355,8 +356,15 @@ router.get('/:id', async (req: Request, res: Response) => {
res.json(applicationWithCompleteness);
} catch (error) {
logger.error('Failed to get application', error);
res.status(500).json({ error: 'Failed to get application' });
logger.error(`Failed to get application ${id}`, error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const errorDetails = error instanceof Error && error.stack ? error.stack : String(error);
logger.debug(`Error details for application ${id}:`, errorDetails);
res.status(500).json({
error: 'Failed to get application',
details: errorMessage,
id: id,
});
}
});
@@ -625,34 +633,101 @@ router.get('/:id/related/:objectType', async (req: Request, res: Response) => {
type RelatedObjectType = Server | Flows | Certificate | Domain | AzureSubscription;
let relatedObjects: RelatedObjectType[] = [];
// Get requested attributes from query string (needed for fallback)
const attributesParam = getQueryString(req, 'attributes');
const requestedAttrs = attributesParam
? attributesParam.split(',').map(a => a.trim())
: [];
logger.debug(`Getting related objects for application ${id}, objectType: ${objectType}, typeName: ${typeName}, requestedAttrs: ${requestedAttrs.join(',') || 'none'}`);
// First try to get from cache
switch (typeName) {
case 'Server':
relatedObjects = await cmdbService.getReferencingObjects<Server>(id, 'Server');
logger.debug(`Found ${relatedObjects.length} Servers referencing application ${id} in cache`);
break;
case 'Flows': {
// Flows reference ApplicationComponents via Source and Target attributes
// We need to find Flows where this ApplicationComponent is the target of the reference
relatedObjects = await cmdbService.getReferencingObjects<Flows>(id, 'Flows');
logger.debug(`Found ${relatedObjects.length} Flows referencing application ${id} in cache`);
break;
}
case 'Certificate':
relatedObjects = await cmdbService.getReferencingObjects<Certificate>(id, 'Certificate');
logger.debug(`Found ${relatedObjects.length} Certificates referencing application ${id} in cache`);
break;
case 'Domain':
relatedObjects = await cmdbService.getReferencingObjects<Domain>(id, 'Domain');
logger.debug(`Found ${relatedObjects.length} Domains referencing application ${id} in cache`);
break;
case 'AzureSubscription':
relatedObjects = await cmdbService.getReferencingObjects<AzureSubscription>(id, 'AzureSubscription');
logger.debug(`Found ${relatedObjects.length} AzureSubscriptions referencing application ${id} in cache`);
break;
default:
relatedObjects = [];
logger.warn(`Unknown object type for related objects: ${typeName}`);
}
// If no objects found in cache, try to fetch from Jira directly as fallback
// This helps when relations haven't been synced yet
if (relatedObjects.length === 0) {
try {
// Get application to get its objectKey
const app = await cmdbService.getObject('ApplicationComponent', id);
if (!app) {
logger.warn(`Application ${id} not found in cache, cannot fetch related objects from Jira`);
} else if (!app.objectKey) {
logger.warn(`Application ${id} has no objectKey, cannot fetch related objects from Jira`);
} else {
logger.info(`No related ${typeName} objects found in cache for application ${id} (${app.objectKey}), trying Jira directly...`);
const { jiraAssetsService } = await import('../services/jiraAssets.js');
// Use the Jira object type name from schema (not our internal typeName)
const { OBJECT_TYPES } = await import('../generated/jira-schema.js');
const jiraTypeDef = OBJECT_TYPES[typeName];
const jiraObjectTypeName = jiraTypeDef?.name || objectType;
logger.debug(`Using Jira object type name: "${jiraObjectTypeName}" for internal type "${typeName}"`);
const jiraResult = await jiraAssetsService.getRelatedObjects(app.objectKey, jiraObjectTypeName, requestedAttrs);
logger.debug(`Jira query returned ${jiraResult?.objects?.length || 0} objects`);
if (jiraResult && jiraResult.objects && jiraResult.objects.length > 0) {
logger.info(`Found ${jiraResult.objects.length} related ${typeName} objects from Jira, caching them...`);
// Batch fetch and cache all objects at once (much more efficient)
const objectIds = jiraResult.objects.map(obj => obj.id.toString());
const cachedObjects = await cmdbService.batchFetchAndCacheObjects(typeName as CMDBObjectTypeName, objectIds);
logger.info(`Successfully batch cached ${cachedObjects.length} of ${jiraResult.objects.length} related ${typeName} objects`);
// Use cached objects, fallback to minimal objects from Jira result if not found
const cachedById = new Map(cachedObjects.map(obj => [obj.id, obj]));
relatedObjects = jiraResult.objects.map((jiraObj) => {
const cached = cachedById.get(jiraObj.id.toString());
if (cached) {
return cached as RelatedObjectType;
}
// Fallback: create minimal object from Jira result
logger.debug(`Creating minimal object for ${jiraObj.id} (${jiraObj.key}) as cache lookup failed`);
return {
id: jiraObj.id.toString(),
objectKey: jiraObj.key,
label: jiraObj.label,
_objectType: typeName,
} as RelatedObjectType;
});
logger.info(`Loaded ${relatedObjects.length} related ${typeName} objects (${relatedObjects.filter(o => o).length} valid)`);
} else {
logger.info(`No related ${typeName} objects found in Jira for application ${app.objectKey}`);
}
}
} catch (error) {
logger.error(`Failed to fetch related ${typeName} objects from Jira as fallback for application ${id}:`, error);
}
}
// Get requested attributes from query string
const attributesParam = getQueryString(req, 'attributes');
const requestedAttrs = attributesParam
? attributesParam.split(',').map(a => a.trim())
: [];
// Format response - must match RelatedObjectsResponse type expected by frontend
const objects = relatedObjects.map(obj => {