Comprehensive admin + user dashboards (production-ready)

This commit is contained in:
Leon Serfaty
2026-06-07 17:54:30 -04:00
parent 155507f21a
commit f033f00379
122 changed files with 7878 additions and 805 deletions
@@ -0,0 +1,45 @@
-- AlterTable
ALTER TABLE "content_flag" ADD COLUMN "severity" TEXT NOT NULL DEFAULT 'medium',
ADD COLUMN "source" TEXT NOT NULL DEFAULT 'system';
-- CreateTable
CREATE TABLE "worker_heartbeat" (
"name" TEXT NOT NULL,
"lastBeatAt" TIMESTAMP(3) NOT NULL,
"queued" INTEGER,
"running" INTEGER,
"meta" JSONB,
CONSTRAINT "worker_heartbeat_pkey" PRIMARY KEY ("name")
);
-- CreateTable
CREATE TABLE "webhook_event" (
"id" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"eventId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'processed',
"error" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "webhook_event_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "webhook_event_eventId_key" ON "webhook_event"("eventId");
-- CreateIndex
CREATE INDEX "webhook_event_provider_createdAt_idx" ON "webhook_event"("provider", "createdAt");
-- CreateIndex
CREATE INDEX "episode_createdAt_idx" ON "episode"("createdAt");
-- CreateIndex
CREATE INDEX "subscription_status_idx" ON "subscription"("status");
-- CreateIndex
CREATE INDEX "subscription_createdAt_idx" ON "subscription"("createdAt");
-- CreateIndex
CREATE INDEX "user_createdAt_idx" ON "user"("createdAt");
@@ -0,0 +1,25 @@
-- AlterTable
ALTER TABLE "episode" ADD COLUMN "shareId" TEXT,
ADD COLUMN "sharedAt" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "user" ADD COLUMN "onboardedAt" TIMESTAMP(3);
-- CreateTable
CREATE TABLE "user_preferences" (
"userId" TEXT NOT NULL,
"defaultVoiceId" TEXT,
"defaultLanguage" TEXT NOT NULL DEFAULT 'en',
"emailOnEpisodeReady" BOOLEAN NOT NULL DEFAULT true,
"productEmails" BOOLEAN NOT NULL DEFAULT true,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "user_preferences_pkey" PRIMARY KEY ("userId")
);
-- CreateIndex
CREATE UNIQUE INDEX "episode_shareId_key" ON "episode"("shareId");
-- AddForeignKey
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+53 -2
View File
@@ -30,6 +30,7 @@ model User {
// app fields
stripeCustomerId String?
paypalPayerId String?
onboardedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -37,13 +38,15 @@ model User {
sessions Session[]
accounts Account[]
memberships Member[]
sentInvitations Invitation[] @relation("SentInvitations")
sentInvitations Invitation[] @relation("SentInvitations")
episodes Episode[]
series Series[]
apiKeys ApiKey[]
usageRecords UsageRecord[]
auditLogs AuditLog[] @relation("ActorLogs")
auditLogs AuditLog[] @relation("ActorLogs")
preferences UserPreferences?
@@index([createdAt])
@@map("user")
}
@@ -210,6 +213,8 @@ model Subscription {
@@index([referenceId])
@@index([stripeSubscriptionId])
@@index([paypalSubscriptionId])
@@index([status])
@@index([createdAt])
@@map("subscription")
}
@@ -290,6 +295,11 @@ model Episode {
stage String? // human-readable current step
errorMessage String?
// Public share: when shareId is set, the episode is reachable at /p/<shareId>
// without auth. Clearing shareId disables the public page.
shareId String? @unique
sharedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -305,6 +315,7 @@ model Episode {
@@index([organizationId])
@@index([seriesId])
@@index([status])
@@index([createdAt])
@@map("episode")
}
@@ -483,6 +494,8 @@ model ContentFlag {
episodeId String
episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
reason String
source String @default("system") // "moderation" | "report" | "system"
severity String @default("medium") // "low" | "medium" | "high"
status String @default("open") // open | reviewed | removed
reviewedBy String?
createdAt DateTime @default(now())
@@ -490,3 +503,41 @@ model ContentFlag {
@@index([status])
@@map("content_flag")
}
/// Liveness of a long-running worker process (heartbeat). One row per worker name.
model WorkerHeartbeat {
name String @id
lastBeatAt DateTime
queued Int?
running Int?
meta Json?
@@map("worker_heartbeat")
}
/// Per-user editor defaults and notification preferences (settings page).
model UserPreferences {
userId String @id
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
defaultVoiceId String?
defaultLanguage String @default("en")
emailOnEpisodeReady Boolean @default(true)
productEmails Boolean @default(true)
updatedAt DateTime @updatedAt
@@map("user_preferences")
}
/// Billing webhook delivery log — also dedups replayed events on `eventId`.
model WebhookEvent {
id String @id @default(cuid())
provider String // "stripe" | "paypal"
eventId String @unique
type String
status String @default("processed") // processed | failed | skipped
error String?
createdAt DateTime @default(now())
@@index([provider, createdAt])
@@map("webhook_event")
}