Add Anthropic API, test environment, remove locale settings
Feature 1: Anthropic API Integration - Add @anthropic-ai/sdk with adapter wrapping OpenAI-shaped interface - Support Claude models (opus, sonnet, haiku) with extended thinking - Auto-reset model on provider switch, JSON retry logic - Add Claude model pricing to ai-usage tracker - Update AI settings form with Anthropic provider option Feature 2: Remove Locale Settings UI - Strip Localization tab from admin settings - Remove i18n settings from router inferCategory and getFeatureFlags - Keep franc document language detection intact Feature 3: Test Environment with Role Impersonation - Add isTest field to User, Program, Project, Competition models - Test environment service: create/teardown with realistic dummy data - JWT-based impersonation for test users (@test.local emails) - Impersonation banner with quick-switch between test roles - Test environment panel in admin settings (SUPER_ADMIN only) - Email redirect: @test.local emails routed to admin with [TEST] prefix - Complete data isolation: 45+ isTest:false filters across platform - All global queries on User/Project/Program/Competition - AI services blocked from processing test data - Cron jobs skip test rounds/users - Analytics/exports exclude test data - Admin layout/pickers hide test programs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,8 @@ const editionOrRoundInput = z.object({
|
||||
})
|
||||
|
||||
function projectWhere(input: { roundId?: string; programId?: string }) {
|
||||
if (input.roundId) return { projectRoundStates: { some: { roundId: input.roundId } } }
|
||||
return { programId: input.programId! }
|
||||
if (input.roundId) return { isTest: false, projectRoundStates: { some: { roundId: input.roundId } } }
|
||||
return { isTest: false, programId: input.programId! }
|
||||
}
|
||||
|
||||
function assignmentWhere(input: { roundId?: string; programId?: string }) {
|
||||
@@ -263,7 +263,7 @@ export const analyticsRouter = router({
|
||||
if (round?.roundType === 'EVALUATION') {
|
||||
// For evaluation rounds, break down by evaluation status per project
|
||||
const projects = await ctx.prisma.projectRoundState.findMany({
|
||||
where: { roundId: input.roundId },
|
||||
where: { roundId: input.roundId, project: { isTest: false } },
|
||||
select: {
|
||||
projectId: true,
|
||||
project: {
|
||||
@@ -309,7 +309,7 @@ export const analyticsRouter = router({
|
||||
// Non-evaluation rounds: use ProjectRoundState
|
||||
const states = await ctx.prisma.projectRoundState.groupBy({
|
||||
by: ['state'],
|
||||
where: { roundId: input.roundId },
|
||||
where: { roundId: input.roundId, project: { isTest: false } },
|
||||
_count: true,
|
||||
})
|
||||
return states.map((s) => ({
|
||||
@@ -469,8 +469,8 @@ export const analyticsRouter = router({
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const where = input.roundId
|
||||
? { assignments: { some: { roundId: input.roundId } } }
|
||||
: { programId: input.programId }
|
||||
? { isTest: false, assignments: { some: { roundId: input.roundId } } }
|
||||
: { isTest: false, programId: input.programId }
|
||||
|
||||
const distribution = await ctx.prisma.project.groupBy({
|
||||
by: ['country'],
|
||||
@@ -537,7 +537,7 @@ export const analyticsRouter = router({
|
||||
|
||||
// Count distinct projects per round via assignments
|
||||
const projectAssignments = await ctx.prisma.assignment.findMany({
|
||||
where: { roundId: { in: roundIds } },
|
||||
where: { roundId: { in: roundIds }, project: { isTest: false } },
|
||||
select: { roundId: true, projectId: true },
|
||||
distinct: ['roundId', 'projectId'],
|
||||
})
|
||||
@@ -714,12 +714,14 @@ export const analyticsRouter = router({
|
||||
const roundId = input?.roundId
|
||||
|
||||
const projectFilter = roundId
|
||||
? { projectRoundStates: { some: { roundId } } }
|
||||
: {}
|
||||
const assignmentFilter = roundId ? { roundId } : {}
|
||||
? { isTest: false, projectRoundStates: { some: { roundId } } }
|
||||
: { isTest: false }
|
||||
const assignmentFilter = roundId
|
||||
? { roundId }
|
||||
: { round: { competition: { isTest: false } } }
|
||||
const evalFilter = roundId
|
||||
? { assignment: { roundId }, status: 'SUBMITTED' as const }
|
||||
: { status: 'SUBMITTED' as const }
|
||||
: { assignment: { round: { competition: { isTest: false } } }, status: 'SUBMITTED' as const }
|
||||
|
||||
const [
|
||||
programCount,
|
||||
@@ -730,9 +732,9 @@ export const analyticsRouter = router({
|
||||
totalAssignments,
|
||||
evaluationScores,
|
||||
] = await Promise.all([
|
||||
ctx.prisma.program.count(),
|
||||
ctx.prisma.program.count({ where: { isTest: false } }),
|
||||
ctx.prisma.round.findMany({
|
||||
where: { status: 'ROUND_ACTIVE' },
|
||||
where: { status: 'ROUND_ACTIVE', competition: { isTest: false } },
|
||||
select: { id: true, name: true },
|
||||
take: 5,
|
||||
}),
|
||||
@@ -743,7 +745,7 @@ export const analyticsRouter = router({
|
||||
select: { userId: true },
|
||||
distinct: ['userId'],
|
||||
}).then((rows) => rows.length)
|
||||
: ctx.prisma.user.count({ where: { role: 'JURY_MEMBER', status: 'ACTIVE' } }),
|
||||
: ctx.prisma.user.count({ where: { isTest: false, role: 'JURY_MEMBER', status: 'ACTIVE' } }),
|
||||
ctx.prisma.evaluation.count({ where: evalFilter }),
|
||||
ctx.prisma.assignment.count({ where: assignmentFilter }),
|
||||
ctx.prisma.evaluation.findMany({
|
||||
@@ -988,7 +990,7 @@ export const analyticsRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const where: Record<string, unknown> = {}
|
||||
const where: Record<string, unknown> = { isTest: false }
|
||||
|
||||
if (input.roundId) {
|
||||
where.projectRoundStates = { some: { roundId: input.roundId } }
|
||||
@@ -1151,15 +1153,15 @@ export const analyticsRouter = router({
|
||||
switch (roundType) {
|
||||
case 'INTAKE': {
|
||||
const [total, byState, byCategory] = await Promise.all([
|
||||
ctx.prisma.projectRoundState.count({ where: { roundId: input.roundId } }),
|
||||
ctx.prisma.projectRoundState.count({ where: { roundId: input.roundId, project: { isTest: false } } }),
|
||||
ctx.prisma.projectRoundState.groupBy({
|
||||
by: ['state'],
|
||||
where: { roundId: input.roundId },
|
||||
where: { roundId: input.roundId, project: { isTest: false } },
|
||||
_count: true,
|
||||
}),
|
||||
ctx.prisma.project.groupBy({
|
||||
by: ['competitionCategory'],
|
||||
where: { projectRoundStates: { some: { roundId: input.roundId } } },
|
||||
where: { isTest: false, projectRoundStates: { some: { roundId: input.roundId } } },
|
||||
_count: true,
|
||||
}),
|
||||
])
|
||||
@@ -1395,7 +1397,7 @@ export const analyticsRouter = router({
|
||||
// Get competition rounds for file grouping
|
||||
let competitionRounds: { id: string; name: string; roundType: string }[] = []
|
||||
const competition = await ctx.prisma.competition.findFirst({
|
||||
where: { programId: projectRaw.programId },
|
||||
where: { programId: projectRaw.programId, isTest: false },
|
||||
include: { rounds: { select: { id: true, name: true, roundType: true }, orderBy: { sortOrder: 'asc' } } },
|
||||
})
|
||||
if (competition) {
|
||||
@@ -1478,9 +1480,23 @@ export const analyticsRouter = router({
|
||||
.query(async ({ ctx, input }) => {
|
||||
const limit = input?.limit ?? 10
|
||||
|
||||
// Exclude actions performed by test users
|
||||
const testUserIds = await ctx.prisma.user.findMany({
|
||||
where: { isTest: true },
|
||||
select: { id: true },
|
||||
}).then((users) => users.map((u) => u.id))
|
||||
|
||||
const entries = await ctx.prisma.decisionAuditLog.findMany({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
...(testUserIds.length > 0 && {
|
||||
where: {
|
||||
OR: [
|
||||
{ actorId: null },
|
||||
{ actorId: { notIn: testUserIds } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
select: {
|
||||
id: true,
|
||||
eventType: true,
|
||||
@@ -1496,7 +1512,7 @@ export const analyticsRouter = router({
|
||||
const actorIds = [...new Set(entries.map((e) => e.actorId).filter(Boolean))] as string[]
|
||||
const actors = actorIds.length > 0
|
||||
? await ctx.prisma.user.findMany({
|
||||
where: { id: { in: actorIds } },
|
||||
where: { id: { in: actorIds }, isTest: false },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
: []
|
||||
|
||||
Reference in New Issue
Block a user