Skip to content

Commit eccf260

Browse files
committed
chore: merge main into release for new releases
2 parents 9b7b47b + 2a4c14d commit eccf260

File tree

57 files changed

+1970
-903
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1970
-903
lines changed

apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { z } from 'zod';
66
// Adjust safe-action import for colocalized structure
77
import { authActionClient } from '@/actions/safe-action';
88
import type { ActionResponse } from '@/actions/types';
9-
import { sendUnassignedItemsNotificationEmail, type UnassignedItem } from '@comp/email';
9+
import {
10+
isUserUnsubscribed,
11+
sendUnassignedItemsNotificationEmail,
12+
type UnassignedItem,
13+
} from '@comp/email';
1014

1115
const removeMemberSchema = z.object({
1216
memberId: z.string(),
@@ -252,15 +256,24 @@ export const removeMember = authActionClient
252256
const removedMemberName = targetMember.user.name || targetMember.user.email || 'Member';
253257

254258
if (owner) {
255-
// Send email to the org owner
256-
sendUnassignedItemsNotificationEmail({
257-
email: owner.user.email,
258-
userName: owner.user.name || owner.user.email || 'Owner',
259-
organizationName: organization.name,
260-
organizationId: ctx.session.activeOrganizationId,
261-
removedMemberName,
262-
unassignedItems,
263-
});
259+
// Check if owner is unsubscribed from unassigned items notifications
260+
const unsubscribed = await isUserUnsubscribed(
261+
db,
262+
owner.user.email,
263+
'unassignedItemsNotifications',
264+
);
265+
266+
if (!unsubscribed) {
267+
// Send email to the org owner
268+
sendUnassignedItemsNotificationEmail({
269+
email: owner.user.email,
270+
userName: owner.user.name || owner.user.email || 'Owner',
271+
organizationName: organization.name,
272+
organizationId: ctx.session.activeOrganizationId,
273+
removedMemberName,
274+
unassignedItems,
275+
});
276+
}
264277
}
265278
}
266279

apps/app/src/app/(app)/[orgId]/security-questionnaire/[questionnaireId]/components/QuestionnaireDetailClient.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,17 @@ export function QuestionnaireDetailClient({
6464
question: r.question,
6565
answer: r.answer,
6666
sources: r.sources,
67-
failedToGenerate: (r as any).failedToGenerate ?? false,
68-
status: (r as any).status ?? 'untouched',
69-
_originalIndex: (r as any).originalIndex ?? index,
67+
failedToGenerate: r.failedToGenerate ?? false,
68+
status: r.status ?? 'untouched',
69+
_originalIndex: r.originalIndex ?? index,
7070
}))}
7171
filteredResults={filteredResults?.map((r, index) => ({
7272
question: r.question,
7373
answer: r.answer,
7474
sources: r.sources,
75-
failedToGenerate: (r as any).failedToGenerate ?? false,
76-
status: (r as any).status ?? 'untouched',
77-
_originalIndex: (r as any).originalIndex ?? index,
75+
failedToGenerate: r.failedToGenerate ?? false,
76+
status: r.status ?? 'untouched',
77+
_originalIndex: r.originalIndex ?? index,
7878
}))}
7979
searchQuery={searchQuery}
8080
setSearchQuery={setSearchQuery}

apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/vendor-questionnaire-orchestrator.ts

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
'use server';
22

33
import { authActionClient } from '@/actions/safe-action';
4-
import { vendorQuestionnaireOrchestratorTask } from '@/jobs/tasks/vendors/vendor-questionnaire-orchestrator';
5-
import { tasks } from '@trigger.dev/sdk';
4+
import { answerQuestion } from '@/jobs/tasks/vendors/answer-question';
5+
import { syncOrganizationEmbeddings } from '@/lib/vector';
6+
import { logger } from '@/utils/logger';
7+
import { headers } from 'next/headers';
8+
import { revalidatePath } from 'next/cache';
69
import { z } from 'zod';
710

811
const inputSchema = z.object({
912
questionsAndAnswers: z.array(
1013
z.object({
1114
question: z.string(),
1215
answer: z.string().nullable(),
16+
_originalIndex: z.number().optional(), // Preserves original index from QuestionnaireResult
1317
}),
1418
),
1519
});
@@ -34,26 +38,99 @@ export const vendorQuestionnaireOrchestrator = authActionClient
3438
const organizationId = session.activeOrganizationId;
3539

3640
try {
37-
// Trigger the root orchestrator task - it will handle batching internally
38-
const handle = await tasks.trigger<typeof vendorQuestionnaireOrchestratorTask>(
39-
'vendor-questionnaire-orchestrator',
40-
{
41-
vendorId: `org_${organizationId}`,
41+
logger.info('Starting auto-answer questionnaire', {
42+
organizationId,
43+
questionCount: questionsAndAnswers.length,
44+
});
45+
46+
// Sync organization embeddings before generating answers
47+
// Uses incremental sync: only updates what changed (much faster than full sync)
48+
try {
49+
await syncOrganizationEmbeddings(organizationId);
50+
logger.info('Organization embeddings synced successfully', {
4251
organizationId,
43-
questionsAndAnswers,
44-
},
52+
});
53+
} catch (error) {
54+
logger.warn('Failed to sync organization embeddings', {
55+
organizationId,
56+
error: error instanceof Error ? error.message : 'Unknown error',
57+
});
58+
// Continue with existing embeddings if sync fails
59+
}
60+
61+
// Filter questions that need answers (skip already answered)
62+
// Preserve original index if provided (for single question answers)
63+
const questionsToAnswer = questionsAndAnswers
64+
.map((qa, index) => ({
65+
...qa,
66+
index: qa._originalIndex !== undefined ? qa._originalIndex : index,
67+
}))
68+
.filter((qa) => !qa.answer || qa.answer.trim().length === 0);
69+
70+
logger.info('Questions to answer', {
71+
total: questionsAndAnswers.length,
72+
toAnswer: questionsToAnswer.length,
73+
});
74+
75+
// Process all questions in parallel by calling answerQuestion directly
76+
// Note: metadata updates are disabled since we're not in a Trigger.dev task context
77+
const results = await Promise.all(
78+
questionsToAnswer.map((qa) =>
79+
answerQuestion(
80+
{
81+
question: qa.question,
82+
organizationId,
83+
questionIndex: qa.index,
84+
totalQuestions: questionsAndAnswers.length,
85+
},
86+
{ useMetadata: false },
87+
),
88+
),
4589
);
4690

91+
// Process results
92+
const allAnswers: Array<{
93+
questionIndex: number;
94+
question: string;
95+
answer: string | null;
96+
sources?: Array<{
97+
sourceType: string;
98+
sourceName?: string;
99+
score: number;
100+
}>;
101+
}> = results.map((result) => ({
102+
questionIndex: result.questionIndex,
103+
question: result.question,
104+
answer: result.answer,
105+
sources: result.sources,
106+
}));
107+
108+
logger.info('Auto-answer questionnaire completed', {
109+
organizationId,
110+
totalQuestions: questionsAndAnswers.length,
111+
answered: allAnswers.filter((a) => a.answer).length,
112+
});
113+
114+
// Revalidate the page to show updated answers
115+
const headersList = await headers();
116+
let path = headersList.get('x-pathname') || headersList.get('referer') || '';
117+
path = path.replace(/\/[a-z]{2}\//, '/');
118+
revalidatePath(path);
119+
47120
return {
48121
success: true,
49122
data: {
50-
taskId: handle.id, // Return orchestrator task ID for polling
123+
answers: allAnswers,
51124
},
52125
};
53126
} catch (error) {
127+
logger.error('Failed to answer questions', {
128+
organizationId,
129+
error: error instanceof Error ? error.message : 'Unknown error',
130+
});
54131
throw error instanceof Error
55132
? error
56-
: new Error('Failed to trigger vendor questionnaire orchestrator');
133+
: new Error('Failed to answer questions');
57134
}
58135
});
59136

apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsCards.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ export function QuestionnaireResultsCards({
5656
<div className="lg:hidden space-y-4">
5757
{filteredResults.map((qa, index) => {
5858
// Use originalIndex if available (from detail page), otherwise find by question text
59-
const originalIndex = (qa as any)._originalIndex !== undefined
60-
? (qa as any)._originalIndex
59+
const originalIndex = qa._originalIndex !== undefined
60+
? qa._originalIndex
6161
: results.findIndex((r) => r.question === qa.question);
6262
// Fallback to index if not found (shouldn't happen, but safety check)
6363
const safeIndex = originalIndex >= 0 ? originalIndex : index;

apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ export function QuestionnaireResultsTable({
6666
<TableBody>
6767
{filteredResults.map((qa, index) => {
6868
// Use originalIndex if available (from detail page), otherwise find by question text
69-
const originalIndex = (qa as any)._originalIndex !== undefined
70-
? (qa as any)._originalIndex
69+
const originalIndex = qa._originalIndex !== undefined
70+
? qa._originalIndex
7171
: results.findIndex((r) => r.question === qa.question);
7272
// Fallback to index if not found (shouldn't happen, but safety check)
7373
const safeIndex = originalIndex >= 0 ? originalIndex : index;

apps/app/src/app/(app)/[orgId]/security-questionnaire/components/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ export interface QuestionAnswer {
1111
}>;
1212
failedToGenerate?: boolean; // Track if auto-generation was attempted but failed
1313
status?: 'untouched' | 'generated' | 'manual'; // Track answer source: untouched, AI-generated, or manually edited
14+
// Optional field used when converting QuestionnaireResult to QuestionAnswer for orchestrator
15+
// Preserves the original index from QuestionnaireResult.originalIndex
16+
_originalIndex?: number;
1417
}
1518

0 commit comments

Comments
 (0)