fix: tech debt batch 1 — TS errors, vulnerabilities, dead code
- Fixed 12 TypeScript errors across analytics.ts, observer-project-detail.tsx, bulk-upload/page.tsx, settings/profile/page.tsx - npm audit: 8 vulnerabilities resolved (1 critical, 4 high, 3 moderate) - Deleted 3 dead files: live-control.ts (618 lines), feature-flags.ts, file-type-categories.ts - Removed typescript.ignoreBuildErrors: true — TS errors now block builds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1379,11 +1379,10 @@ export const analyticsRouter = router({
|
||||
bucket: true, objectKey: true, pageCount: true, textPreview: true,
|
||||
detectedLang: true, langConfidence: true, analyzedAt: true,
|
||||
roundId: true,
|
||||
round: { select: { id: true, name: true, roundType: true, sortOrder: true } },
|
||||
requirementId: true,
|
||||
requirement: { select: { id: true, name: true, description: true, isRequired: true } },
|
||||
},
|
||||
orderBy: [{ round: { sortOrder: 'asc' } }, { createdAt: 'asc' }],
|
||||
orderBy: [{ createdAt: 'asc' }],
|
||||
},
|
||||
teamMembers: {
|
||||
include: {
|
||||
|
||||
@@ -1,618 +0,0 @@
|
||||
/**
|
||||
* Live Control Service
|
||||
*
|
||||
* Manages real-time control of live final events within a round.
|
||||
* Handles session management, project cursor navigation, queue reordering,
|
||||
* pause/resume, and cohort voting windows.
|
||||
*
|
||||
* The LiveProgressCursor tracks the current position in a live presentation
|
||||
* sequence. Cohorts group projects for voting with configurable windows.
|
||||
*/
|
||||
|
||||
import type { PrismaClient } from '@prisma/client'
|
||||
import { logAudit } from '@/server/utils/audit'
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SessionResult {
|
||||
success: boolean
|
||||
sessionId: string | null
|
||||
cursorId: string | null
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
export interface CursorState {
|
||||
roundId: string
|
||||
sessionId: string
|
||||
activeProjectId: string | null
|
||||
activeOrderIndex: number
|
||||
isPaused: boolean
|
||||
totalProjects: number
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function generateSessionId(): string {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const random = Math.random().toString(36).substring(2, 8)
|
||||
return `live-${timestamp}-${random}`
|
||||
}
|
||||
|
||||
// ─── Start Session ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Create or reset a LiveProgressCursor for a round. If a cursor already exists,
|
||||
* it is reset to the beginning. A new sessionId is always generated.
|
||||
*/
|
||||
export async function startSession(
|
||||
roundId: string,
|
||||
actorId: string,
|
||||
prisma: PrismaClient | any
|
||||
): Promise<SessionResult> {
|
||||
try {
|
||||
// Verify round exists and is a LIVE_FINAL type
|
||||
const round = await prisma.round.findUnique({
|
||||
where: { id: roundId },
|
||||
})
|
||||
|
||||
if (!round) {
|
||||
return {
|
||||
success: false,
|
||||
sessionId: null,
|
||||
cursorId: null,
|
||||
errors: [`Round ${roundId} not found`],
|
||||
}
|
||||
}
|
||||
|
||||
if (round.roundType !== 'LIVE_FINAL') {
|
||||
return {
|
||||
success: false,
|
||||
sessionId: null,
|
||||
cursorId: null,
|
||||
errors: [
|
||||
`Round "${round.name}" is type ${round.roundType}, expected LIVE_FINAL`,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// Find the first project in the first cohort
|
||||
const firstCohortProject = await prisma.cohortProject.findFirst({
|
||||
where: {
|
||||
cohort: { roundId },
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' as const },
|
||||
select: { projectId: true },
|
||||
})
|
||||
|
||||
const sessionId = generateSessionId()
|
||||
|
||||
// Upsert the cursor (one per round)
|
||||
const cursor = await prisma.liveProgressCursor.upsert({
|
||||
where: { roundId },
|
||||
create: {
|
||||
roundId,
|
||||
sessionId,
|
||||
activeProjectId: firstCohortProject?.projectId ?? null,
|
||||
activeOrderIndex: 0,
|
||||
isPaused: false,
|
||||
},
|
||||
update: {
|
||||
sessionId,
|
||||
activeProjectId: firstCohortProject?.projectId ?? null,
|
||||
activeOrderIndex: 0,
|
||||
isPaused: false,
|
||||
},
|
||||
})
|
||||
|
||||
// Decision audit log
|
||||
await prisma.decisionAuditLog.create({
|
||||
data: {
|
||||
eventType: 'live.session_started',
|
||||
entityType: 'LiveProgressCursor',
|
||||
entityId: cursor.id,
|
||||
actorId,
|
||||
detailsJson: {
|
||||
roundId,
|
||||
sessionId,
|
||||
firstProjectId: firstCohortProject?.projectId ?? null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await logAudit({
|
||||
prisma,
|
||||
userId: actorId,
|
||||
action: 'LIVE_SESSION_STARTED',
|
||||
entityType: 'LiveProgressCursor',
|
||||
entityId: cursor.id,
|
||||
detailsJson: { roundId, sessionId },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionId,
|
||||
cursorId: cursor.id,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LiveControl] Failed to start session:', error)
|
||||
return {
|
||||
success: false,
|
||||
sessionId: null,
|
||||
cursorId: null,
|
||||
errors: [
|
||||
error instanceof Error ? error.message : 'Failed to start live session',
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Set Active Project ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Set the currently active project in the live session.
|
||||
* Validates that the project belongs to a cohort in this round and performs
|
||||
* a version check on the cursor's sessionId to prevent stale updates.
|
||||
*/
|
||||
export async function setActiveProject(
|
||||
roundId: string,
|
||||
projectId: string,
|
||||
actorId: string,
|
||||
prisma: PrismaClient | any
|
||||
): Promise<{ success: boolean; errors?: string[] }> {
|
||||
try {
|
||||
// Verify cursor exists
|
||||
const cursor = await prisma.liveProgressCursor.findUnique({
|
||||
where: { roundId },
|
||||
})
|
||||
|
||||
if (!cursor) {
|
||||
return {
|
||||
success: false,
|
||||
errors: ['No live session found for this round. Start a session first.'],
|
||||
}
|
||||
}
|
||||
|
||||
// Verify project is in a cohort for this round
|
||||
const cohortProject = await prisma.cohortProject.findFirst({
|
||||
where: {
|
||||
projectId,
|
||||
cohort: { roundId },
|
||||
},
|
||||
select: { id: true, sortOrder: true },
|
||||
})
|
||||
|
||||
if (!cohortProject) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
`Project ${projectId} is not in any cohort for round ${roundId}`,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// Update cursor
|
||||
await prisma.liveProgressCursor.update({
|
||||
where: { roundId },
|
||||
data: {
|
||||
activeProjectId: projectId,
|
||||
activeOrderIndex: cohortProject.sortOrder,
|
||||
},
|
||||
})
|
||||
|
||||
// Audit
|
||||
await prisma.decisionAuditLog.create({
|
||||
data: {
|
||||
eventType: 'live.cursor_updated',
|
||||
entityType: 'LiveProgressCursor',
|
||||
entityId: cursor.id,
|
||||
actorId,
|
||||
detailsJson: {
|
||||
roundId,
|
||||
projectId,
|
||||
orderIndex: cohortProject.sortOrder,
|
||||
action: 'setActiveProject',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await logAudit({
|
||||
prisma,
|
||||
userId: actorId,
|
||||
action: 'LIVE_SET_ACTIVE_PROJECT',
|
||||
entityType: 'LiveProgressCursor',
|
||||
entityId: cursor.id,
|
||||
detailsJson: { projectId, orderIndex: cohortProject.sortOrder },
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('[LiveControl] Failed to set active project:', error)
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to set active project',
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Jump to Project ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Jump to a project by its order index in the cohort queue.
|
||||
*/
|
||||
export async function jumpToProject(
|
||||
roundId: string,
|
||||
orderIndex: number,
|
||||
actorId: string,
|
||||
prisma: PrismaClient | any
|
||||
): Promise<{ success: boolean; projectId?: string; errors?: string[] }> {
|
||||
try {
|
||||
const cursor = await prisma.liveProgressCursor.findUnique({
|
||||
where: { roundId },
|
||||
})
|
||||
|
||||
if (!cursor) {
|
||||
return {
|
||||
success: false,
|
||||
errors: ['No live session found for this round'],
|
||||
}
|
||||
}
|
||||
|
||||
// Find the CohortProject at the given sort order
|
||||
const cohortProject = await prisma.cohortProject.findFirst({
|
||||
where: {
|
||||
cohort: { roundId },
|
||||
sortOrder: orderIndex,
|
||||
},
|
||||
select: { projectId: true, sortOrder: true },
|
||||
})
|
||||
|
||||
if (!cohortProject) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [`No project found at order index ${orderIndex}`],
|
||||
}
|
||||
}
|
||||
|
||||
// Update cursor
|
||||
await prisma.liveProgressCursor.update({
|
||||
where: { roundId },
|
||||
data: {
|
||||
activeProjectId: cohortProject.projectId,
|
||||
activeOrderIndex: orderIndex,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.decisionAuditLog.create({
|
||||
data: {
|
||||
eventType: 'live.cursor_updated',
|
||||
entityType: 'LiveProgressCursor',
|
||||
entityId: cursor.id,
|
||||
actorId,
|
||||
detailsJson: {
|
||||
roundId,
|
||||
projectId: cohortProject.projectId,
|
||||
orderIndex,
|
||||
action: 'jumpToProject',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await logAudit({
|
||||
prisma,
|
||||
userId: actorId,
|
||||
action: 'LIVE_JUMP_TO_PROJECT',
|
||||
entityType: 'LiveProgressCursor',
|
||||
entityId: cursor.id,
|
||||
detailsJson: { orderIndex, projectId: cohortProject.projectId },
|
||||
})
|
||||
|
||||
return { success: true, projectId: cohortProject.projectId }
|
||||
} catch (error) {
|
||||
console.error('[LiveControl] Failed to jump to project:', error)
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
error instanceof Error ? error.message : 'Failed to jump to project',
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Reorder Queue ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Reorder the presentation queue by updating CohortProject sortOrder values.
|
||||
* newOrder is an array of cohortProjectIds in the desired order.
|
||||
*/
|
||||
export async function reorderQueue(
|
||||
roundId: string,
|
||||
newOrder: string[],
|
||||
actorId: string,
|
||||
prisma: PrismaClient | any
|
||||
): Promise<{ success: boolean; errors?: string[] }> {
|
||||
try {
|
||||
// Verify all provided IDs belong to cohorts in this round
|
||||
const cohortProjects = await prisma.cohortProject.findMany({
|
||||
where: {
|
||||
id: { in: newOrder },
|
||||
cohort: { roundId },
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
const validIds = new Set(cohortProjects.map((cp: any) => cp.id))
|
||||
const invalidIds = newOrder.filter((id) => !validIds.has(id))
|
||||
|
||||
if (invalidIds.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
`CohortProject IDs not found in round ${roundId}: ${invalidIds.join(', ')}`,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// Update sortOrder for each item
|
||||
await prisma.$transaction(
|
||||
newOrder.map((cohortProjectId, index) =>
|
||||
prisma.cohortProject.update({
|
||||
where: { id: cohortProjectId },
|
||||
data: { sortOrder: index },
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const cursor = await prisma.liveProgressCursor.findUnique({
|
||||
where: { roundId },
|
||||
})
|
||||
|
||||
if (cursor) {
|
||||
await prisma.decisionAuditLog.create({
|
||||
data: {
|
||||
eventType: 'live.queue_reordered',
|
||||
entityType: 'LiveProgressCursor',
|
||||
entityId: cursor.id,
|
||||
actorId,
|
||||
detailsJson: {
|
||||
roundId,
|
||||
newOrderCount: newOrder.length,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await logAudit({
|
||||
prisma,
|
||||
userId: actorId,
|
||||
action: 'LIVE_REORDER_QUEUE',
|
||||
entityType: 'Round',
|
||||
entityId: roundId,
|
||||
detailsJson: { reorderedCount: newOrder.length },
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('[LiveControl] Failed to reorder queue:', error)
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
error instanceof Error ? error.message : 'Failed to reorder queue',
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Pause / Resume ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Toggle the pause state of a live session.
|
||||
*/
|
||||
export async function pauseResume(
|
||||
roundId: string,
|
||||
isPaused: boolean,
|
||||
actorId: string,
|
||||
prisma: PrismaClient | any
|
||||
): Promise<{ success: boolean; errors?: string[] }> {
|
||||
try {
|
||||
const cursor = await prisma.liveProgressCursor.findUnique({
|
||||
where: { roundId },
|
||||
})
|
||||
|
||||
if (!cursor) {
|
||||
return {
|
||||
success: false,
|
||||
errors: ['No live session found for this round'],
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.liveProgressCursor.update({
|
||||
where: { roundId },
|
||||
data: { isPaused },
|
||||
})
|
||||
|
||||
await prisma.decisionAuditLog.create({
|
||||
data: {
|
||||
eventType: isPaused ? 'live.session_paused' : 'live.session_resumed',
|
||||
entityType: 'LiveProgressCursor',
|
||||
entityId: cursor.id,
|
||||
actorId,
|
||||
detailsJson: {
|
||||
roundId,
|
||||
isPaused,
|
||||
sessionId: cursor.sessionId,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await logAudit({
|
||||
prisma,
|
||||
userId: actorId,
|
||||
action: isPaused ? 'LIVE_SESSION_PAUSED' : 'LIVE_SESSION_RESUMED',
|
||||
entityType: 'LiveProgressCursor',
|
||||
entityId: cursor.id,
|
||||
detailsJson: { roundId, isPaused },
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('[LiveControl] Failed to pause/resume:', error)
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to toggle pause state',
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Cohort Window Management ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Open a cohort's voting window. Sets isOpen to true and records the
|
||||
* window open timestamp.
|
||||
*/
|
||||
export async function openCohortWindow(
|
||||
cohortId: string,
|
||||
actorId: string,
|
||||
prisma: PrismaClient | any
|
||||
): Promise<{ success: boolean; errors?: string[] }> {
|
||||
try {
|
||||
const cohort = await prisma.cohort.findUnique({
|
||||
where: { id: cohortId },
|
||||
})
|
||||
|
||||
if (!cohort) {
|
||||
return { success: false, errors: [`Cohort ${cohortId} not found`] }
|
||||
}
|
||||
|
||||
if (cohort.isOpen) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [`Cohort "${cohort.name}" is already open`],
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
|
||||
await prisma.cohort.update({
|
||||
where: { id: cohortId },
|
||||
data: {
|
||||
isOpen: true,
|
||||
windowOpenAt: now,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.decisionAuditLog.create({
|
||||
data: {
|
||||
eventType: 'live.cohort_opened',
|
||||
entityType: 'Cohort',
|
||||
entityId: cohortId,
|
||||
actorId,
|
||||
detailsJson: {
|
||||
cohortName: cohort.name,
|
||||
roundId: cohort.roundId,
|
||||
openedAt: now.toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await logAudit({
|
||||
prisma,
|
||||
userId: actorId,
|
||||
action: 'LIVE_COHORT_OPENED',
|
||||
entityType: 'Cohort',
|
||||
entityId: cohortId,
|
||||
detailsJson: { cohortName: cohort.name, roundId: cohort.roundId },
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('[LiveControl] Failed to open cohort window:', error)
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to open cohort window',
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a cohort's voting window. Sets isOpen to false and records the
|
||||
* window close timestamp.
|
||||
*/
|
||||
export async function closeCohortWindow(
|
||||
cohortId: string,
|
||||
actorId: string,
|
||||
prisma: PrismaClient | any
|
||||
): Promise<{ success: boolean; errors?: string[] }> {
|
||||
try {
|
||||
const cohort = await prisma.cohort.findUnique({
|
||||
where: { id: cohortId },
|
||||
})
|
||||
|
||||
if (!cohort) {
|
||||
return { success: false, errors: [`Cohort ${cohortId} not found`] }
|
||||
}
|
||||
|
||||
if (!cohort.isOpen) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [`Cohort "${cohort.name}" is already closed`],
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
|
||||
await prisma.cohort.update({
|
||||
where: { id: cohortId },
|
||||
data: {
|
||||
isOpen: false,
|
||||
windowCloseAt: now,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.decisionAuditLog.create({
|
||||
data: {
|
||||
eventType: 'live.cohort_closed',
|
||||
entityType: 'Cohort',
|
||||
entityId: cohortId,
|
||||
actorId,
|
||||
detailsJson: {
|
||||
cohortName: cohort.name,
|
||||
roundId: cohort.roundId,
|
||||
closedAt: now.toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await logAudit({
|
||||
prisma,
|
||||
userId: actorId,
|
||||
action: 'LIVE_COHORT_CLOSED',
|
||||
entityType: 'Cohort',
|
||||
entityId: cohortId,
|
||||
detailsJson: { cohortName: cohort.name, roundId: cohort.roundId },
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('[LiveControl] Failed to close cohort window:', error)
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to close cohort window',
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user