feat: per-round advancement selection, email preview, Docker/auth fixes
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m42s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m42s
- Bulk notification dialog: per-round checkboxes (default none selected), selected count badge, "Preview Email" button with rendered iframe - Backend: roundIds filter on sendBulkPassedNotifications, new previewAdvancementEmail query - Docker: add external MinIO network so app container can reach MinIO - File router: try/catch on getPresignedUrl with descriptive error - Auth: custom NextAuth logger suppresses CredentialsSignin stack traces Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -111,9 +111,18 @@ export const fileRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
const url = await getPresignedUrl(input.bucket, input.objectKey, 'GET', 900,
|
||||
input.forDownload ? { downloadFileName: input.fileName || input.objectKey.split('/').pop() || 'download' } : undefined
|
||||
) // 15 min
|
||||
let url: string
|
||||
try {
|
||||
url = await getPresignedUrl(input.bucket, input.objectKey, 'GET', 900,
|
||||
input.forDownload ? { downloadFileName: input.fileName || input.objectKey.split('/').pop() || 'download' } : undefined
|
||||
) // 15 min
|
||||
} catch (err) {
|
||||
console.error('[file] getPresignedUrl failed:', input.objectKey, err instanceof Error ? err.message : err)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: `File not available in storage: ${err instanceof Error ? err.message : 'unknown error'}`,
|
||||
})
|
||||
}
|
||||
|
||||
// Log file access
|
||||
await logAudit({
|
||||
|
||||
@@ -1670,20 +1670,63 @@ export const projectRouter = router({
|
||||
* Groups by round, determines next round, sends via batch sender.
|
||||
* Skips projects that have already been notified (unless skipAlreadySent=false).
|
||||
*/
|
||||
previewAdvancementEmail: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
roundId: z.string(),
|
||||
customMessage: z.string().optional(),
|
||||
fullCustomBody: z.boolean().default(false),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const round = await ctx.prisma.round.findUnique({
|
||||
where: { id: input.roundId },
|
||||
select: {
|
||||
name: true,
|
||||
competitionId: true,
|
||||
},
|
||||
})
|
||||
if (!round) throw new TRPCError({ code: 'NOT_FOUND', message: 'Round not found' })
|
||||
|
||||
const rounds = await ctx.prisma.round.findMany({
|
||||
where: { competitionId: round.competitionId },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
const idx = rounds.findIndex((r) => r.id === input.roundId)
|
||||
const nextRound = rounds[idx + 1]
|
||||
|
||||
const { getAdvancementNotificationTemplate } = await import('@/lib/email')
|
||||
const template = getAdvancementNotificationTemplate(
|
||||
'Team Lead Name',
|
||||
'Example Project Title',
|
||||
round.name,
|
||||
nextRound?.name ?? 'Next Round',
|
||||
input.customMessage || undefined,
|
||||
undefined,
|
||||
input.fullCustomBody,
|
||||
)
|
||||
return { subject: template.subject, html: template.html }
|
||||
}),
|
||||
|
||||
sendBulkPassedNotifications: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
customMessage: z.string().optional(),
|
||||
fullCustomBody: z.boolean().default(false),
|
||||
skipAlreadySent: z.boolean().default(true),
|
||||
roundIds: z.array(z.string()).optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { customMessage, fullCustomBody, skipAlreadySent } = input
|
||||
const { customMessage, fullCustomBody, skipAlreadySent, roundIds } = input
|
||||
|
||||
// Find all PASSED project round states
|
||||
// Find all PASSED project round states (optionally filtered by round)
|
||||
const passedStates = await ctx.prisma.projectRoundState.findMany({
|
||||
where: { state: 'PASSED' },
|
||||
where: {
|
||||
state: 'PASSED',
|
||||
...(roundIds && roundIds.length > 0 ? { roundId: { in: roundIds } } : {}),
|
||||
},
|
||||
select: {
|
||||
projectId: true,
|
||||
roundId: true,
|
||||
|
||||
Reference in New Issue
Block a user