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

@@ -0,0 +1,256 @@
/**
* Schema Cache Service
*
* In-memory cache for schema data with TTL support.
* Provides fast access to schema information without hitting the database on every request.
*/
import { logger } from './logger.js';
import { schemaDiscoveryService } from './schemaDiscoveryService.js';
import type { ObjectTypeDefinition, AttributeDefinition } from '../generated/jira-schema.js';
import { getDatabaseAdapter } from './database/singleton.js';
interface SchemaResponse {
metadata: {
generatedAt: string;
objectTypeCount: number;
totalAttributes: number;
enabledObjectTypeCount?: number;
};
objectTypes: Record<string, ObjectTypeWithLinks>;
cacheCounts?: Record<string, number>;
jiraCounts?: Record<string, number>;
}
interface ObjectTypeWithLinks extends ObjectTypeDefinition {
enabled: boolean; // Whether this object type is enabled for syncing
incomingLinks: Array<{
fromType: string;
fromTypeName: string;
attributeName: string;
isMultiple: boolean;
}>;
outgoingLinks: Array<{
toType: string;
toTypeName: string;
attributeName: string;
isMultiple: boolean;
}>;
}
class SchemaCacheService {
private cache: SchemaResponse | null = null;
private cacheTimestamp: number = 0;
private readonly CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
private db = getDatabaseAdapter(); // Use shared database adapter singleton
/**
* Get schema from cache or fetch from database
*/
async getSchema(): Promise<SchemaResponse> {
// Check cache validity
const now = Date.now();
if (this.cache && (now - this.cacheTimestamp) < this.CACHE_TTL_MS) {
logger.debug('SchemaCache: Returning cached schema');
return this.cache;
}
// Cache expired or doesn't exist - fetch from database
logger.debug('SchemaCache: Cache expired or missing, fetching from database');
const schema = await this.fetchFromDatabase();
// Update cache
this.cache = schema;
this.cacheTimestamp = now;
return schema;
}
/**
* Invalidate cache (force refresh on next request)
*/
invalidate(): void {
logger.debug('SchemaCache: Invalidating cache');
this.cache = null;
this.cacheTimestamp = 0;
}
/**
* Fetch schema from database and build response
* Returns ALL object types (enabled and disabled) with their sync status
*/
private async fetchFromDatabase(): Promise<SchemaResponse> {
// Schema discovery must be manually triggered via API endpoints
// No automatic discovery on first run
// Fetch ALL object types (enabled and disabled) with their schema info
const objectTypeRows = await this.db.query<{
id: number;
schema_id: number;
jira_type_id: number;
type_name: string;
display_name: string;
description: string | null;
sync_priority: number;
object_count: number;
enabled: boolean | number;
}>(
`SELECT ot.id, ot.schema_id, ot.jira_type_id, ot.type_name, ot.display_name, ot.description, ot.sync_priority, ot.object_count, ot.enabled
FROM object_types ot
ORDER BY ot.sync_priority, ot.type_name`
);
if (objectTypeRows.length === 0) {
// No types found, return empty schema
return {
metadata: {
generatedAt: new Date().toISOString(),
objectTypeCount: 0,
totalAttributes: 0,
},
objectTypes: {},
};
}
// Fetch attributes for ALL object types using JOIN
const attributeRows = await this.db.query<{
id: number;
jira_attr_id: number;
object_type_name: string;
attr_name: string;
field_name: string;
attr_type: string;
is_multiple: boolean | number;
is_editable: boolean | number;
is_required: boolean | number;
is_system: boolean | number;
reference_type_name: string | null;
description: string | null;
position: number | null;
schema_id: number;
type_name: string;
}>(
`SELECT a.*, ot.schema_id, ot.type_name
FROM attributes a
INNER JOIN object_types ot ON a.object_type_name = ot.type_name
ORDER BY ot.type_name, COALESCE(a.position, 0), a.jira_attr_id`
);
logger.debug(`SchemaCache: Found ${objectTypeRows.length} object types (enabled and disabled) and ${attributeRows.length} attributes`);
// Build object types with attributes
// Use type_name as key (even if same type exists in multiple schemas, we'll show the first enabled one)
// In practice, if same type_name exists in multiple schemas, attributes should be the same
const objectTypesWithLinks: Record<string, ObjectTypeWithLinks> = {};
for (const typeRow of objectTypeRows) {
const typeName = typeRow.type_name;
// Skip if we already have this type_name (first enabled one wins)
if (objectTypesWithLinks[typeName]) {
logger.debug(`SchemaCache: Skipping duplicate type_name ${typeName} from schema ${typeRow.schema_id}`);
continue;
}
// Match attributes by both schema_id and type_name to ensure correct mapping
const matchingAttributes = attributeRows.filter(a => a.schema_id === typeRow.schema_id && a.type_name === typeName);
logger.debug(`SchemaCache: Found ${matchingAttributes.length} attributes for ${typeName} (schema_id: ${typeRow.schema_id})`);
const attributes = matchingAttributes.map(attrRow => {
// Convert boolean/number for SQLite compatibility
const isMultiple = typeof attrRow.is_multiple === 'boolean' ? attrRow.is_multiple : attrRow.is_multiple === 1;
const isEditable = typeof attrRow.is_editable === 'boolean' ? attrRow.is_editable : attrRow.is_editable === 1;
const isRequired = typeof attrRow.is_required === 'boolean' ? attrRow.is_required : attrRow.is_required === 1;
const isSystem = typeof attrRow.is_system === 'boolean' ? attrRow.is_system : attrRow.is_system === 1;
return {
jiraId: attrRow.jira_attr_id,
name: attrRow.attr_name,
fieldName: attrRow.field_name,
type: attrRow.attr_type as AttributeDefinition['type'],
isMultiple,
isEditable,
isRequired,
isSystem,
referenceTypeName: attrRow.reference_type_name || undefined,
description: attrRow.description || undefined,
position: attrRow.position ?? 0,
} as AttributeDefinition;
});
// Convert enabled boolean/number to boolean
const isEnabled = typeof typeRow.enabled === 'boolean' ? typeRow.enabled : typeRow.enabled === 1;
objectTypesWithLinks[typeName] = {
jiraTypeId: typeRow.jira_type_id,
name: typeRow.display_name,
typeName: typeName,
syncPriority: typeRow.sync_priority,
objectCount: typeRow.object_count,
enabled: isEnabled,
attributes,
incomingLinks: [],
outgoingLinks: [],
};
}
// Build link relationships
for (const [typeName, typeDef] of Object.entries(objectTypesWithLinks)) {
for (const attr of typeDef.attributes) {
if (attr.type === 'reference' && attr.referenceTypeName) {
// Add outgoing link from this type
typeDef.outgoingLinks.push({
toType: attr.referenceTypeName,
toTypeName: objectTypesWithLinks[attr.referenceTypeName]?.name || attr.referenceTypeName,
attributeName: attr.name,
isMultiple: attr.isMultiple,
});
// Add incoming link to the referenced type
if (objectTypesWithLinks[attr.referenceTypeName]) {
objectTypesWithLinks[attr.referenceTypeName].incomingLinks.push({
fromType: typeName,
fromTypeName: typeDef.name,
attributeName: attr.name,
isMultiple: attr.isMultiple,
});
}
}
}
}
// Get cache counts (objectsByType) if available
let cacheCounts: Record<string, number> | undefined;
try {
const { dataService } = await import('./dataService.js');
const cacheStatus = await dataService.getCacheStatus();
cacheCounts = cacheStatus.objectsByType;
} catch (err) {
logger.debug('SchemaCache: Could not fetch cache counts', err);
// Continue without cache counts - not critical
}
// Calculate metadata (include enabled count)
const totalAttributes = Object.values(objectTypesWithLinks).reduce(
(sum, t) => sum + t.attributes.length,
0
);
const enabledCount = Object.values(objectTypesWithLinks).filter(t => t.enabled).length;
const response: SchemaResponse = {
metadata: {
generatedAt: new Date().toISOString(),
objectTypeCount: objectTypeRows.length,
totalAttributes,
enabledObjectTypeCount: enabledCount,
},
objectTypes: objectTypesWithLinks,
cacheCounts,
};
return response;
}
}
// Export singleton instance
export const schemaCacheService = new SchemaCacheService();