added email reinitaialize and api token limit

This commit is contained in:
laxmanhalaki 2026-01-21 18:41:10 +05:30
parent 9285c97d4b
commit d1ae0ffaec
10 changed files with 552 additions and 521 deletions

View File

@ -1,2 +1,2 @@
import{a as t}from"./index-XMUlTorM.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
//# sourceMappingURL=conclusionApi-muWiQD3D.js.map
import{a as t}from"./index-D5U31xpx.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
//# sourceMappingURL=conclusionApi-xBwvOJP0.js.map

View File

@ -1 +1 @@
{"version":3,"file":"conclusionApi-muWiQD3D.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
{"version":3,"file":"conclusionApi-xBwvOJP0.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -52,7 +52,7 @@
transition: transform 0.2s ease;
}
</style>
<script type="module" crossorigin src="/assets/index-XMUlTorM.js"></script>
<script type="module" crossorigin src="/assets/index-D5U31xpx.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">

View File

@ -10,6 +10,7 @@ import { seedDefaultConfigurations } from './services/configSeed.service';
import { startPauseResumeJob } from './jobs/pauseResumeJob';
import './queues/pauseResumeWorker'; // Initialize pause resume worker
import { initializeQueueMetrics, stopQueueMetrics } from './utils/queueMetrics';
import { emailService } from './services/email.service';
const PORT: number = parseInt(process.env.PORT || '5000', 10);
@ -20,6 +21,15 @@ const startServer = async (): Promise<void> => {
// This will merge secrets from GCS into process.env if enabled
await initializeSecrets();
// Re-initialize email service after secrets are loaded (in case SMTP credentials were loaded)
// This ensures the email service uses production SMTP if credentials are available
try {
await emailService.initialize();
console.log('📧 Email service re-initialized after secrets loaded');
} catch (error) {
console.warn('⚠️ Email service re-initialization warning (will use test account if SMTP not configured):', error);
}
const server = http.createServer(app);
initSocket(server);

View File

@ -99,10 +99,11 @@ class AIService {
try {
// Get the generative model
// Increase maxOutputTokens to handle longer conclusions (up to ~4000 tokens ≈ 3000 words)
const generativeModel = this.vertexAI.getGenerativeModel({
model: this.model,
generationConfig: {
maxOutputTokens: 2048,
maxOutputTokens: 4096, // Increased from 2048 to handle longer conclusions
temperature: 0.3,
},
});
@ -154,6 +155,19 @@ class AIService {
// Extract text from response
const text = candidate.content?.parts?.[0]?.text || '';
// Handle MAX_TOKENS finish reason - accept whatever response we got
// We trust the AI's response - no truncation on our side
if (candidate.finishReason === 'MAX_TOKENS' && text) {
// Accept the response as-is - AI was instructed to stay within limits
// If it hit the limit, we still use what we got (no truncation on our side)
logger.info('[AI Service] Vertex AI response hit token limit, but content received is preserved as-is:', {
textLength: text.length,
finishReason: candidate.finishReason
});
// Return the response without any truncation - trust what AI generated
return text;
}
if (!text) {
// Log detailed response structure for debugging
logger.error('[AI Service] Empty text in Vertex AI response:', {
@ -169,7 +183,7 @@ class AIService {
if (candidate.finishReason === 'SAFETY') {
throw new Error('Vertex AI blocked the response due to safety filters. The prompt may contain content that violates safety policies.');
} else if (candidate.finishReason === 'MAX_TOKENS') {
throw new Error('Vertex AI response was truncated due to token limit.');
throw new Error('Vertex AI response was truncated due to token limit. The prompt may be too long or the response limit was exceeded.');
} else if (candidate.finishReason === 'RECITATION') {
throw new Error('Vertex AI blocked the response due to recitation concerns.');
} else {
@ -254,9 +268,10 @@ class AIService {
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
const maxLength = parseInt(maxLengthStr || '2000', 10);
// Log length (no trimming - preserve complete AI-generated content)
// Trust AI's response - do not truncate anything
// AI is instructed to stay within limit, but we accept whatever it generates
if (remarkText.length > maxLength) {
logger.warn(`[AI Service] ⚠️ AI exceeded suggested limit (${remarkText.length} > ${maxLength}). Content preserved to avoid incomplete information.`);
logger.info(`[AI Service] AI generated ${remarkText.length} characters (suggested limit: ${maxLength}). Full content preserved as-is.`);
}
// Extract key points (look for bullet points or numbered items)
@ -336,8 +351,9 @@ class AIService {
.map((wn: any) => `- ${wn.userName}: "${wn.message.substring(0, 150)}${wn.message.length > 150 ? '...' : ''}"`)
.join('\n');
// Summarize documents
// Summarize documents (limit to reduce token usage)
const documentSummary = documents
.slice(0, 10) // Limit to first 10 documents
.map((d: any) => `- ${d.fileName} (by ${d.uploadedBy})`)
.join('\n');
@ -382,15 +398,17 @@ ${isRejected
- Sounds natural and human-written (not AI-generated)`}
**CRITICAL CHARACTER LIMIT - STRICT REQUIREMENT:**
- Your response MUST be EXACTLY within ${maxLength} characters (not words, CHARACTERS including spaces)
- Count your characters carefully before responding
- Your response MUST stay within ${maxLength} characters (not words, CHARACTERS including spaces including HTML tags)
- This is a HARD LIMIT - you must count your characters and ensure your complete response fits within ${maxLength} characters
- Count your characters carefully before responding - include all HTML tags in your count
- If you have too much content, PRIORITIZE the most important information:
1. Final decision (approved/rejected)
2. Key approvers and their decisions
3. Critical TAT breaches (if any)
4. Brief summary of the request
- OMIT less important details to fit within the limit rather than exceeding it
- Better to be concise than to exceed the limit
- Better to be concise and complete within the limit than to exceed it
- IMPORTANT: Generate your complete response within this limit - do not generate partial content that exceeds the limit
**WRITING GUIDELINES:**
- Be concise and direct - every word must add value

View File

@ -100,6 +100,18 @@ export class EmailService {
await this.initialize();
}
// If using test account, check if SMTP credentials are now available and re-initialize
if (this.useTestAccount) {
const smtpHost = process.env.SMTP_HOST;
const smtpUser = process.env.SMTP_USER;
const smtpPassword = process.env.SMTP_PASSWORD;
if (smtpHost && smtpUser && smtpPassword) {
logger.info('📧 SMTP credentials detected - re-initializing email service with production SMTP');
await this.initialize();
}
}
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@royalenfield.com>';
@ -233,6 +245,8 @@ export class EmailService {
export const emailService = new EmailService();
// Initialize on import (will use test account if SMTP not configured)
// Note: If secrets are loaded later, the service will re-initialize automatically
// when sendEmail is called (if SMTP credentials become available)
emailService.initialize().catch(error => {
logger.error('Failed to initialize email service:', error);
});

View File

@ -1831,14 +1831,16 @@ export class WorkflowService {
// Include PAUSED status so paused requests where user is the current approver are shown
const pendingLevels = await ApprovalLevel.findAll({
where: {
status: { [Op.in]: [
status: {
[Op.in]: [
ApprovalStatus.PENDING as any,
(ApprovalStatus as any).IN_PROGRESS ?? 'IN_PROGRESS',
ApprovalStatus.PAUSED as any,
'PENDING',
'IN_PROGRESS',
'PAUSED'
] as any },
] as any
},
},
order: [['requestId', 'ASC'], ['levelNumber', 'ASC']],
attributes: ['requestId', 'levelNumber', 'approverId'],
@ -1907,7 +1909,8 @@ export class WorkflowService {
baseConditions.push({
[Op.or]: [
{
status: { [Op.in]: [
status: {
[Op.in]: [
WorkflowStatus.PENDING as any,
WorkflowStatus.APPROVED as any,
WorkflowStatus.PAUSED as any,
@ -1915,7 +1918,8 @@ export class WorkflowService {
'IN_PROGRESS', // Legacy support - will be migrated to PENDING
'APPROVED',
'PAUSED'
] as any }
] as any
}
},
// Also include requests with isPaused = true (even if status is PENDING)
{
@ -1942,10 +1946,12 @@ export class WorkflowService {
baseConditions.push({
[Op.and]: [
{ status: statusUpper },
{ [Op.or]: [
{
[Op.or]: [
{ isPaused: { [Op.is]: null } },
{ isPaused: false }
]}
]
}
]
});
}
@ -2067,12 +2073,14 @@ export class WorkflowService {
const levelRows = await ApprovalLevel.findAll({
where: {
approverId: userId,
status: { [Op.in]: [
status: {
[Op.in]: [
ApprovalStatus.APPROVED as any,
(ApprovalStatus as any).REJECTED ?? 'REJECTED',
'APPROVED',
'REJECTED'
] as any },
] as any
},
},
attributes: ['requestId'],
});
@ -2378,40 +2386,6 @@ export class WorkflowService {
userAgent: requestMetadata?.userAgent || undefined
});
// Send notification to INITIATOR confirming submission
await notificationService.sendToUsers([initiatorId], {
title: 'Request Submitted Successfully',
body: `Your request "${workflowData.title}" has been submitted and is now with the first approver.`,
requestNumber: requestNumber,
requestId: (workflow as any).requestId,
url: `/request/${requestNumber}`,
type: 'request_submitted',
priority: 'MEDIUM'
});
// Send notification to FIRST APPROVER for assignment
const firstLevel = await ApprovalLevel.findOne({ where: { requestId: (workflow as any).requestId, levelNumber: 1 } });
if (firstLevel) {
await notificationService.sendToUsers([(firstLevel as any).approverId], {
title: 'New Request Assigned',
body: `${workflowData.title}`,
requestNumber: requestNumber,
requestId: (workflow as any).requestId,
url: `/request/${requestNumber}`,
type: 'assignment',
priority: 'HIGH',
actionRequired: true
});
activityService.log({
requestId: (workflow as any).requestId,
type: 'assignment',
user: { userId: initiatorId, name: initiatorName },
timestamp: new Date().toISOString(),
action: 'Assigned to approver',
details: `Request assigned to ${(firstLevel as any).approverName || (firstLevel as any).approverEmail || 'approver'} for review`
});
}
return workflow;
} catch (error) {
@ -2549,7 +2523,7 @@ export class WorkflowService {
// Reload with associations
const workflow = await WorkflowRequest.findByPk(actualRequestId, {
include: [ { association: 'initiator' } ]
include: [{ association: 'initiator' }]
});
if (!workflow) return null;
@ -2645,7 +2619,7 @@ export class WorkflowService {
// Use the actual UUID requestId for all queries
const approvals = await ApprovalLevel.findAll({
where: { requestId: actualRequestId },
order: [['levelNumber','ASC']]
order: [['levelNumber', 'ASC']]
}) as any[];
const participants = await Participant.findAll({
@ -3192,13 +3166,28 @@ export class WorkflowService {
// Don't fail the submission if TAT scheduling fails
}
// NOTE: Notifications are already sent in createWorkflow() when the workflow is created
// We should NOT send "Request submitted" to the approver here - that's incorrect
// The approver should only receive "New Request Assigned" notification (sent in createWorkflow)
// The initiator receives "Request Submitted Successfully" notification (sent in createWorkflow)
//
// If this is a draft being submitted, notifications were already sent during creation,
// so we don't need to send them again here to avoid duplicates
// Send "Request Submitted" notification to INITIATOR
await notificationService.sendToUsers([initiatorId], {
title: 'Request Submitted Successfully',
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`,
requestNumber: (updated as any).requestNumber,
requestId: (updated as any).requestId,
url: `/request/${(updated as any).requestNumber}`,
type: 'request_submitted',
priority: 'MEDIUM'
});
// Send "New Request Assigned" notification to FIRST APPROVER
await notificationService.sendToUsers([(current as any).approverId], {
title: 'New Request Assigned',
body: `${workflowTitle}`,
requestNumber: (updated as any).requestNumber,
requestId: (updated as any).requestId,
url: `/request/${(updated as any).requestNumber}`,
type: 'assignment',
priority: 'HIGH',
actionRequired: true
});
}
return updated;
} catch (error) {