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[] resourceAccess ResourceAccess[]
submittedProjects Project[] @relation("ProjectSubmittedBy") submittedProjects Project[] @relation("ProjectSubmittedBy")
liveVotes LiveVote[] liveVotes LiveVote[]
liveNotes LiveNote[]
// Team membership & mentorship // Team membership & mentorship
teamMemberships TeamMember[] teamMemberships TeamMember[]
@@ -657,6 +658,10 @@ model Project {
finalistConfirmation FinalistConfirmation? finalistConfirmation FinalistConfirmation?
externalLunchAttendees ExternalAttendee[] externalLunchAttendees ExternalAttendee[]
// Grand-finale ceremony
audienceFavoriteVotes AudienceFavoriteVote[]
liveNotes LiveNote[]
@@index([programId]) @@index([programId])
@@index([status]) @@index([status])
@@index([tags]) @@index([tags])
@@ -1188,13 +1193,22 @@ model LiveVotingSession {
audienceRequireId Boolean @default(false) // Require email/phone for audience audienceRequireId Boolean @default(false) // Require email/phone for audience
audienceVotingDuration Int? // Minutes (null = same as jury) 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()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// Relations // Relations
round Round? @relation(fields: [roundId], references: [id], onDelete: Cascade) round Round? @relation(fields: [roundId], references: [id], onDelete: Cascade)
votes LiveVote[] votes LiveVote[]
audienceVoters AudienceVoter[] audienceVoters AudienceVoter[]
favoriteVotes AudienceFavoriteVote[]
revealState RevealState?
@@index([status]) @@index([status])
} }
@@ -1211,6 +1225,9 @@ model LiveVote {
// Criteria scores (used when votingMode="criteria") // Criteria scores (used when votingMode="criteria")
criterionScoresJson Json? @db.JsonB // { [criterionId]: score } - null for simple mode 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 // Audience voter link
audienceVoterId String? audienceVoterId String?
@@ -1238,13 +1255,81 @@ model AudienceVoter {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
// Relations // Relations
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
votes LiveVote[] votes LiveVote[]
favoriteVotes AudienceFavoriteVote[]
@@index([sessionId]) @@index([sessionId])
@@index([token]) @@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 // TEAM MEMBERSHIP
// ============================================================================= // =============================================================================
@@ -2157,6 +2242,15 @@ model LiveProgressCursor {
activeOrderIndex Int @default(0) activeOrderIndex Int @default(0)
isPaused Boolean @default(false) 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()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -2283,6 +2377,7 @@ model Round {
notificationLogs NotificationLog[] notificationLogs NotificationLog[]
cohorts Cohort[] cohorts Cohort[]
liveCursor LiveProgressCursor? liveCursor LiveProgressCursor?
liveNotes LiveNote[]
@@unique([competitionId, slug]) @@unique([competitionId, slug])
@@unique([competitionId, sortOrder]) @@unique([competitionId, sortOrder])
@@ -2984,7 +3079,7 @@ model ExternalAttendee {
dishId String? dishId String?
allergens Allergen[] @default([]) allergens Allergen[] @default([])
allergenOther String? allergenOther String?
inviteSentAt DateTime? // when the dish-selection email was last sent (null = never invited) inviteSentAt DateTime? // when the dish-selection email was last sent (null = never invited)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt