feat: Runtime configuration and Docker deployment improvements
Frontend: - Add runtime configuration service for deployment-time API URL injection - Create docker-entrypoint.sh to generate config.json from environment variables - Update ApiService, ApprovalService, and DocumentViewer to use RuntimeConfigService - Add APP_INITIALIZER to load runtime config before app starts Backend: - Fix init-blockchain.js to properly quote mnemonic phrases in .env file - Improve docker-entrypoint.sh with health checks and better error handling Docker: - Add API_BASE_URL environment variable to frontend container - Update docker-compose.yml with clear documentation for remote deployment - Reorganize .env.example with clear categories (REQUIRED FOR REMOTE, PRODUCTION, AUTO-GENERATED) Workflow fixes: - Fix DepartmentApproval interface to match backend schema - Fix stage transformation for 0-indexed stageOrder - Fix workflow list to show correct stage count from definition.stages Cleanup: - Move development artifacts to .trash directory - Remove root-level package.json (was only for utility scripts) - Add .trash/ to .gitignore
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ethers } from 'ethers';
|
||||
import { LicenseRequest } from '../../database/models/license-request.model';
|
||||
import { Applicant } from '../../database/models/applicant.model';
|
||||
import { Department } from '../../database/models/department.model';
|
||||
@@ -12,16 +13,24 @@ import { AuditLog } from '../../database/models/audit-log.model';
|
||||
import { DepartmentsService } from '../departments/departments.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
|
||||
export interface StatusCount {
|
||||
status: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface PlatformStats {
|
||||
totalRequests: number;
|
||||
requestsByStatus: Record<string, number>;
|
||||
totalApprovals: number;
|
||||
requestsByStatus: StatusCount[];
|
||||
totalApplicants: number;
|
||||
activeApplicants: number;
|
||||
totalDepartments: number;
|
||||
activeDepartments: number;
|
||||
totalDocuments: number;
|
||||
totalBlockchainTransactions: number;
|
||||
transactionsByStatus: Record<string, number>;
|
||||
transactionsByStatus: StatusCount[];
|
||||
averageProcessingTime: number;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
export interface SystemHealth {
|
||||
@@ -70,44 +79,43 @@ export class AdminService {
|
||||
transactionsByStatus,
|
||||
] = await Promise.all([
|
||||
this.requestModel.query().resultSize(),
|
||||
this.requestModel
|
||||
.query()
|
||||
.select('status')
|
||||
.count('* as count')
|
||||
.groupBy('status') as any,
|
||||
this.requestModel.query().select('status').count('* as count').groupBy('status') as any,
|
||||
this.applicantModel.query().resultSize(),
|
||||
this.applicantModel.query().where({ isActive: true }).resultSize(),
|
||||
this.applicantModel.query().where({ is_active: true }).resultSize(),
|
||||
this.departmentModel.query().resultSize(),
|
||||
this.departmentModel.query().where({ isActive: true }).resultSize(),
|
||||
this.departmentModel.query().where({ is_active: true }).resultSize(),
|
||||
this.documentModel.query().resultSize(),
|
||||
this.blockchainTxModel.query().resultSize(),
|
||||
this.blockchainTxModel
|
||||
.query()
|
||||
.select('status')
|
||||
.count('* as count')
|
||||
.groupBy('status') as any,
|
||||
this.blockchainTxModel.query().select('status').count('* as count').groupBy('status') as any,
|
||||
]);
|
||||
|
||||
const statusMap: Record<string, number> = {};
|
||||
for (const row of requestsByStatus) {
|
||||
statusMap[(row as any).status] = parseInt((row as any).count, 10);
|
||||
}
|
||||
// Convert to array format expected by frontend
|
||||
const statusArray: StatusCount[] = requestsByStatus.map((row: any) => ({
|
||||
status: row.status,
|
||||
count: parseInt(row.count, 10),
|
||||
}));
|
||||
|
||||
const txStatusMap: Record<string, number> = {};
|
||||
for (const row of transactionsByStatus) {
|
||||
txStatusMap[(row as any).status] = parseInt((row as any).count, 10);
|
||||
}
|
||||
const txStatusArray: StatusCount[] = transactionsByStatus.map((row: any) => ({
|
||||
status: row.status,
|
||||
count: parseInt(row.count, 10),
|
||||
}));
|
||||
|
||||
// Calculate total approvals
|
||||
const approvedCount = statusArray.find(s => s.status === 'APPROVED')?.count || 0;
|
||||
|
||||
return {
|
||||
totalRequests,
|
||||
requestsByStatus: statusMap,
|
||||
totalApprovals: approvedCount,
|
||||
requestsByStatus: statusArray,
|
||||
totalApplicants,
|
||||
activeApplicants,
|
||||
totalDepartments,
|
||||
activeDepartments,
|
||||
totalDocuments,
|
||||
totalBlockchainTransactions,
|
||||
transactionsByStatus: txStatusMap,
|
||||
transactionsByStatus: txStatusArray,
|
||||
averageProcessingTime: 4.5, // Placeholder
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,8 +129,7 @@ export class AdminService {
|
||||
dbStatus = 'down';
|
||||
}
|
||||
|
||||
const overallStatus =
|
||||
dbStatus === 'up' ? 'healthy' : 'unhealthy';
|
||||
const overallStatus = dbStatus === 'up' ? 'healthy' : 'unhealthy';
|
||||
|
||||
return {
|
||||
status: overallStatus,
|
||||
@@ -138,17 +145,10 @@ export class AdminService {
|
||||
}
|
||||
|
||||
async getRecentActivity(limit: number = 20): Promise<AuditLog[]> {
|
||||
return this.auditLogModel
|
||||
.query()
|
||||
.orderBy('created_at', 'DESC')
|
||||
.limit(limit);
|
||||
return this.auditLogModel.query().orderBy('created_at', 'DESC').limit(limit);
|
||||
}
|
||||
|
||||
async getBlockchainTransactions(
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
status?: string,
|
||||
) {
|
||||
async getBlockchainTransactions(page: number = 1, limit: number = 20, status?: string) {
|
||||
const query = this.blockchainTxModel.query().orderBy('created_at', 'DESC');
|
||||
|
||||
if (status) {
|
||||
@@ -177,7 +177,8 @@ export class AdminService {
|
||||
department: result.department,
|
||||
apiKey: result.apiKey,
|
||||
apiSecret: result.apiSecret,
|
||||
message: 'Department onboarded successfully. Please save the API credentials as they will not be shown again.',
|
||||
message:
|
||||
'Department onboarded successfully. Please save the API credentials as they will not be shown again.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,7 +198,8 @@ export class AdminService {
|
||||
const result = await this.departmentsService.regenerateApiKey(id);
|
||||
return {
|
||||
...result,
|
||||
message: 'API key regenerated successfully. Please save the new credentials as they will not be shown again.',
|
||||
message:
|
||||
'API key regenerated successfully. Please save the new credentials as they will not be shown again.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,9 +223,7 @@ export class AdminService {
|
||||
eventType?: string,
|
||||
contractAddress?: string,
|
||||
) {
|
||||
const query = this.blockchainEventModel
|
||||
.query()
|
||||
.orderBy('created_at', 'DESC');
|
||||
const query = this.blockchainEventModel.query().orderBy('created_at', 'DESC');
|
||||
|
||||
if (eventType) {
|
||||
query.where({ eventType });
|
||||
@@ -255,9 +255,7 @@ export class AdminService {
|
||||
module?: string,
|
||||
search?: string,
|
||||
) {
|
||||
const query = this.appLogModel
|
||||
.query()
|
||||
.orderBy('created_at', 'DESC');
|
||||
const query = this.appLogModel.query().orderBy('created_at', 'DESC');
|
||||
|
||||
if (level) {
|
||||
query.where({ level });
|
||||
@@ -293,7 +291,9 @@ export class AdminService {
|
||||
const documents = await this.documentModel
|
||||
.query()
|
||||
.where({ requestId })
|
||||
.withGraphFetched('[uploadedByUser, versions.[uploadedByUser], departmentReviews.[department]]')
|
||||
.withGraphFetched(
|
||||
'[uploadedByUser, versions.[uploadedByUser], departmentReviews.[department]]',
|
||||
)
|
||||
.orderBy('created_at', 'DESC');
|
||||
|
||||
// Transform documents to include formatted data
|
||||
@@ -309,22 +309,24 @@ export class AdminService {
|
||||
uploadedAt: doc.createdAt,
|
||||
uploadedBy: doc.uploadedByUser?.name || 'Unknown',
|
||||
currentVersion: doc.version || 1,
|
||||
versions: doc.versions?.map((v: any) => ({
|
||||
id: v.id,
|
||||
version: v.version,
|
||||
fileHash: v.fileHash,
|
||||
uploadedAt: v.createdAt,
|
||||
uploadedBy: v.uploadedByUser?.name || 'Unknown',
|
||||
changes: v.changes,
|
||||
})) || [],
|
||||
departmentReviews: doc.departmentReviews?.map((review: any) => ({
|
||||
departmentCode: review.department?.code || 'UNKNOWN',
|
||||
departmentName: review.department?.name || 'Unknown Department',
|
||||
reviewedAt: review.createdAt,
|
||||
reviewedBy: review.reviewedByUser?.name || 'Unknown',
|
||||
status: review.status,
|
||||
comments: review.comments,
|
||||
})) || [],
|
||||
versions:
|
||||
doc.versions?.map((v: any) => ({
|
||||
id: v.id,
|
||||
version: v.version,
|
||||
fileHash: v.fileHash,
|
||||
uploadedAt: v.createdAt,
|
||||
uploadedBy: v.uploadedByUser?.name || 'Unknown',
|
||||
changes: v.changes,
|
||||
})) || [],
|
||||
departmentReviews:
|
||||
doc.departmentReviews?.map((review: any) => ({
|
||||
departmentCode: review.department?.code || 'UNKNOWN',
|
||||
departmentName: review.department?.name || 'Unknown Department',
|
||||
reviewedAt: review.createdAt,
|
||||
reviewedBy: review.reviewedByUser?.name || 'Unknown',
|
||||
status: review.status,
|
||||
comments: review.comments,
|
||||
})) || [],
|
||||
metadata: {
|
||||
mimeType: doc.mimeType,
|
||||
width: doc.width,
|
||||
@@ -333,4 +335,41 @@ export class AdminService {
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async getBlockchainBlocks(limit: number = 5) {
|
||||
this.logger.debug(`Fetching ${limit} recent blockchain blocks`);
|
||||
|
||||
try {
|
||||
const rpcUrl = this.configService.get<string>('BESU_RPC_URL') || 'http://besu-node-1:8545';
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
|
||||
const latestBlockNumber = await provider.getBlockNumber();
|
||||
const blocks = [];
|
||||
|
||||
for (let i = 0; i < limit && latestBlockNumber - i >= 0; i++) {
|
||||
const blockNumber = latestBlockNumber - i;
|
||||
const block = await provider.getBlock(blockNumber);
|
||||
|
||||
if (block) {
|
||||
blocks.push({
|
||||
blockNumber: block.number,
|
||||
hash: block.hash,
|
||||
parentHash: block.parentHash,
|
||||
timestamp: new Date(block.timestamp * 1000).toISOString(),
|
||||
transactionCount: block.transactions.length,
|
||||
gasUsed: Number(block.gasUsed),
|
||||
gasLimit: Number(block.gasLimit),
|
||||
miner: block.miner,
|
||||
nonce: block.nonce,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { data: blocks };
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to fetch blockchain blocks', error);
|
||||
// Return empty array on error - frontend will use mock data
|
||||
return { data: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user