feat(finale): schema for phases, audience windows, favorite votes, notes, reveal

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-10 18:04:25 +02:00
parent 64f88890f5
commit a66bd728cd
2 changed files with 3196 additions and 2997 deletions

View File

@@ -0,0 +1,104 @@
-- CreateEnum
CREATE TYPE "LivePhase" AS ENUM ('ON_DECK', 'PRESENTING', 'QA', 'SCORING');
-- CreateEnum
CREATE TYPE "AudiencePhase" AS ENUM ('CLOSED', 'OPEN');
-- AlterTable
ALTER TABLE "LiveProgressCursor" ADD COLUMN "overrideSlide" TEXT,
ADD COLUMN "phaseDurationSeconds" INTEGER,
ADD COLUMN "phasePausedAccumMs" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "phasePausedAt" TIMESTAMP(3),
ADD COLUMN "phaseStartedAt" TIMESTAMP(3),
ADD COLUMN "projectPhase" "LivePhase" NOT NULL DEFAULT 'ON_DECK',
ADD COLUMN "timingLogJson" JSONB;
-- AlterTable
ALTER TABLE "LiveVote" ADD COLUMN "comment" TEXT;
-- AlterTable
ALTER TABLE "LiveVotingSession" ADD COLUMN "allowOverallFavorite" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "audiencePhase" "AudiencePhase" NOT NULL DEFAULT 'CLOSED',
ADD COLUMN "audienceWindowClosesAt" TIMESTAMP(3),
ADD COLUMN "audienceWindowKey" TEXT,
ADD COLUMN "audienceWindowOpenedAt" TIMESTAMP(3);
-- CreateTable
CREATE TABLE "AudienceFavoriteVote" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"windowKey" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"audienceVoterId" TEXT NOT NULL,
"ipAddress" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AudienceFavoriteVote_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LiveNote" (
"id" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"content" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "LiveNote_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RevealState" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'DRAFT',
"stepsJson" JSONB NOT NULL,
"currentStepIndex" INTEGER NOT NULL DEFAULT -1,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RevealState_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "AudienceFavoriteVote_sessionId_windowKey_ipAddress_idx" ON "AudienceFavoriteVote"("sessionId", "windowKey", "ipAddress");
-- CreateIndex
CREATE INDEX "AudienceFavoriteVote_sessionId_windowKey_projectId_idx" ON "AudienceFavoriteVote"("sessionId", "windowKey", "projectId");
-- CreateIndex
CREATE UNIQUE INDEX "AudienceFavoriteVote_sessionId_windowKey_audienceVoterId_key" ON "AudienceFavoriteVote"("sessionId", "windowKey", "audienceVoterId");
-- CreateIndex
CREATE INDEX "LiveNote_userId_idx" ON "LiveNote"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "LiveNote_roundId_projectId_userId_key" ON "LiveNote"("roundId", "projectId", "userId");
-- CreateIndex
CREATE UNIQUE INDEX "RevealState_sessionId_key" ON "RevealState"("sessionId");
-- AddForeignKey
ALTER TABLE "AudienceFavoriteVote" ADD CONSTRAINT "AudienceFavoriteVote_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AudienceFavoriteVote" ADD CONSTRAINT "AudienceFavoriteVote_audienceVoterId_fkey" FOREIGN KEY ("audienceVoterId") REFERENCES "AudienceVoter"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AudienceFavoriteVote" ADD CONSTRAINT "AudienceFavoriteVote_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LiveNote" ADD CONSTRAINT "LiveNote_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LiveNote" ADD CONSTRAINT "LiveNote_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LiveNote" ADD CONSTRAINT "LiveNote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RevealState" ADD CONSTRAINT "RevealState_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -347,6 +347,7 @@ model User {
resourceAccess ResourceAccess[]
submittedProjects Project[] @relation("ProjectSubmittedBy")
liveVotes LiveVote[]
liveNotes LiveNote[]
// Team membership & mentorship
teamMemberships TeamMember[]
@@ -657,6 +658,10 @@ model Project {
finalistConfirmation FinalistConfirmation?
externalLunchAttendees ExternalAttendee[]
// Grand-finale ceremony
audienceFavoriteVotes AudienceFavoriteVote[]
liveNotes LiveNote[]
@@index([programId])
@@index([status])
@@index([tags])
@@ -1188,6 +1193,13 @@ model LiveVotingSession {
audienceRequireId Boolean @default(false) // Require email/phone for audience
audienceVotingDuration Int? // Minutes (null = same as jury)
// Audience favorite-vote window (grand finale)
audiencePhase AudiencePhase @default(CLOSED)
audienceWindowKey String? // 'CATEGORY:STARTUP' | 'CATEGORY:BUSINESS_CONCEPT' | 'OVERALL'
audienceWindowOpenedAt DateTime?
audienceWindowClosesAt DateTime?
allowOverallFavorite Boolean @default(false) // admin toggle, decided day-of
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1195,6 +1207,8 @@ model LiveVotingSession {
round Round? @relation(fields: [roundId], references: [id], onDelete: Cascade)
votes LiveVote[]
audienceVoters AudienceVoter[]
favoriteVotes AudienceFavoriteVote[]
revealState RevealState?
@@index([status])
}
@@ -1211,6 +1225,9 @@ model LiveVote {
// Criteria scores (used when votingMode="criteria")
criterionScoresJson Json? @db.JsonB // { [criterionId]: score } - null for simple mode
// Optional overall comment from the juror (grand finale)
comment String? @db.Text
// Audience voter link
audienceVoterId String?
@@ -1240,11 +1257,79 @@ model AudienceVoter {
// Relations
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
votes LiveVote[]
favoriteVotes AudienceFavoriteVote[]
@@index([sessionId])
@@index([token])
}
// One pick-one-favorite vote per audience member per voting window.
// windowKey snapshots LiveVotingSession.audienceWindowKey at cast time so
// per-category and overall votes coexist in one table.
model AudienceFavoriteVote {
id String @id @default(cuid())
sessionId String
windowKey String // 'CATEGORY:STARTUP' | 'CATEGORY:BUSINESS_CONCEPT' | 'OVERALL'
projectId String
audienceVoterId String
ipAddress String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
audienceVoter AudienceVoter @relation(fields: [audienceVoterId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([sessionId, windowKey, audienceVoterId])
@@index([sessionId, windowKey, ipAddress])
@@index([sessionId, windowKey, projectId])
}
// Per-juror per-project free-text notes taken during the live ceremony.
// Resurfaces during deliberation.
model LiveNote {
id String @id @default(cuid())
roundId String
projectId String
userId String
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([roundId, projectId, userId])
@@index([userId])
}
// Admin-driven results reveal for the big-screen ceremony view.
// Steps beyond currentStepIndex are never exposed publicly.
model RevealState {
id String @id @default(cuid())
sessionId String @unique
status String @default("DRAFT") // DRAFT | ARMED | REVEALING | DONE
stepsJson Json @db.JsonB // RevealStep[]
currentStepIndex Int @default(-1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
}
enum LivePhase {
ON_DECK
PRESENTING
QA
SCORING
}
enum AudiencePhase {
CLOSED
OPEN
}
// =============================================================================
// TEAM MEMBERSHIP
// =============================================================================
@@ -2157,6 +2242,15 @@ model LiveProgressCursor {
activeOrderIndex Int @default(0)
isPaused Boolean @default(false)
// Per-project ceremony phase + server-stamped timer (grand finale)
projectPhase LivePhase @default(ON_DECK)
phaseStartedAt DateTime?
phaseDurationSeconds Int?
phasePausedAt DateTime?
phasePausedAccumMs Int @default(0)
timingLogJson Json? @db.JsonB // [{projectId, phase, startedAt, endedAt, configuredSeconds, overranSeconds}]
overrideSlide String? // big-screen override: 'welcome' | 'break' | 'deliberation' | 'thanks'
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -2283,6 +2377,7 @@ model Round {
notificationLogs NotificationLog[]
cohorts Cohort[]
liveCursor LiveProgressCursor?
liveNotes LiveNote[]
@@unique([competitionId, slug])
@@unique([competitionId, sortOrder])