586 行
24 KiB
TypeScript
586 行
24 KiB
TypeScript
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, json, float, uniqueIndex } from "drizzle-orm/mysql-core";
|
|
|
|
/**
|
|
* Core user table - supports both OAuth and simple username login
|
|
*/
|
|
export const users = mysqlTable("users", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
openId: varchar("openId", { length: 64 }).notNull().unique(),
|
|
name: text("name"),
|
|
email: varchar("email", { length: 320 }),
|
|
loginMethod: varchar("loginMethod", { length: 64 }),
|
|
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
|
/** Tennis skill level */
|
|
skillLevel: mysqlEnum("skillLevel", ["beginner", "intermediate", "advanced"]).default("beginner"),
|
|
/** User's training goals */
|
|
trainingGoals: text("trainingGoals"),
|
|
/** NTRP rating (1.0 - 5.0) */
|
|
ntrpRating: float("ntrpRating").default(1.5),
|
|
/** Total training sessions completed */
|
|
totalSessions: int("totalSessions").default(0),
|
|
/** Total training minutes */
|
|
totalMinutes: int("totalMinutes").default(0),
|
|
/** Current consecutive check-in streak */
|
|
currentStreak: int("currentStreak").default(0),
|
|
/** Longest ever streak */
|
|
longestStreak: int("longestStreak").default(0),
|
|
/** Total shots across all analyses */
|
|
totalShots: int("totalShots").default(0),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
|
|
});
|
|
|
|
export type User = typeof users.$inferSelect;
|
|
export type InsertUser = typeof users.$inferInsert;
|
|
|
|
/**
|
|
* Simple username-based login accounts
|
|
*/
|
|
export const usernameAccounts = mysqlTable("username_accounts", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
username: varchar("username", { length: 64 }).notNull().unique(),
|
|
userId: int("userId").notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type UsernameAccount = typeof usernameAccounts.$inferSelect;
|
|
|
|
/**
|
|
* Training plans generated for users
|
|
*/
|
|
export const trainingPlans = mysqlTable("training_plans", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
skillLevel: mysqlEnum("skillLevel", ["beginner", "intermediate", "advanced"]).notNull(),
|
|
/** Plan duration in days */
|
|
durationDays: int("durationDays").notNull().default(7),
|
|
/** JSON array of training exercises */
|
|
exercises: json("exercises").notNull(),
|
|
/** Whether this plan is currently active */
|
|
isActive: int("isActive").notNull().default(1),
|
|
/** Auto-adjustment notes from AI analysis */
|
|
adjustmentNotes: text("adjustmentNotes"),
|
|
/** Plan generation version for tracking adjustments */
|
|
version: int("version").notNull().default(1),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type TrainingPlan = typeof trainingPlans.$inferSelect;
|
|
export type InsertTrainingPlan = typeof trainingPlans.$inferInsert;
|
|
|
|
/**
|
|
* Training videos uploaded by users
|
|
*/
|
|
export const trainingVideos = mysqlTable("training_videos", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
/** S3 file key */
|
|
fileKey: varchar("fileKey", { length: 512 }).notNull(),
|
|
/** CDN URL for the video */
|
|
url: text("url").notNull(),
|
|
/** Video format: webm or mp4 */
|
|
format: varchar("format", { length: 16 }).notNull(),
|
|
/** File size in bytes */
|
|
fileSize: int("fileSize"),
|
|
/** Duration in seconds */
|
|
duration: float("duration"),
|
|
/** Type of exercise in the video */
|
|
exerciseType: varchar("exerciseType", { length: 64 }),
|
|
/** Analysis status */
|
|
analysisStatus: mysqlEnum("analysisStatus", ["pending", "analyzing", "completed", "failed"]).default("pending"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type TrainingVideo = typeof trainingVideos.$inferSelect;
|
|
export type InsertTrainingVideo = typeof trainingVideos.$inferInsert;
|
|
|
|
/**
|
|
* Pose analysis results from MediaPipe - enhanced with tennis_analysis features
|
|
*/
|
|
export const poseAnalyses = mysqlTable("pose_analyses", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
videoId: int("videoId").notNull(),
|
|
userId: int("userId").notNull(),
|
|
/** Overall pose score (0-100) */
|
|
overallScore: float("overallScore"),
|
|
/** JSON object with detailed joint angles and metrics */
|
|
poseMetrics: json("poseMetrics"),
|
|
/** JSON array of detected issues */
|
|
detectedIssues: json("detectedIssues"),
|
|
/** JSON array of correction suggestions */
|
|
corrections: json("corrections"),
|
|
/** Exercise type analyzed */
|
|
exerciseType: varchar("exerciseType", { length: 64 }),
|
|
/** Number of frames analyzed */
|
|
framesAnalyzed: int("framesAnalyzed"),
|
|
/** --- tennis_analysis inspired fields --- */
|
|
/** Number of swings/shots detected */
|
|
shotCount: int("shotCount").default(0),
|
|
/** Average swing speed (estimated from keypoint displacement, px/frame) */
|
|
avgSwingSpeed: float("avgSwingSpeed"),
|
|
/** Max swing speed detected */
|
|
maxSwingSpeed: float("maxSwingSpeed"),
|
|
/** Total body movement distance in pixels */
|
|
totalMovementDistance: float("totalMovementDistance"),
|
|
/** Stroke consistency score (0-100) */
|
|
strokeConsistency: float("strokeConsistency"),
|
|
/** Footwork score (0-100) */
|
|
footworkScore: float("footworkScore"),
|
|
/** Fluidity/smoothness score (0-100) */
|
|
fluidityScore: float("fluidityScore"),
|
|
/** JSON array of key moments [{frame, type, description}] */
|
|
keyMoments: json("keyMoments"),
|
|
/** JSON array of movement trajectory points [{x, y, frame}] */
|
|
movementTrajectory: json("movementTrajectory"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type PoseAnalysis = typeof poseAnalyses.$inferSelect;
|
|
export type InsertPoseAnalysis = typeof poseAnalyses.$inferInsert;
|
|
|
|
/**
|
|
* Training session records for progress tracking
|
|
*/
|
|
export const trainingRecords = mysqlTable("training_records", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
planId: int("planId"),
|
|
/** Exercise name/type */
|
|
exerciseName: varchar("exerciseName", { length: 128 }).notNull(),
|
|
exerciseType: varchar("exerciseType", { length: 64 }),
|
|
/** Source of the training fact */
|
|
sourceType: varchar("sourceType", { length: 32 }).default("manual"),
|
|
/** Reference id from source system */
|
|
sourceId: varchar("sourceId", { length: 64 }),
|
|
/** Optional linked video */
|
|
videoId: int("videoId"),
|
|
/** Optional linked plan match */
|
|
linkedPlanId: int("linkedPlanId"),
|
|
matchConfidence: float("matchConfidence"),
|
|
actionCount: int("actionCount").default(0),
|
|
metadata: json("metadata"),
|
|
/** Duration in minutes */
|
|
durationMinutes: int("durationMinutes"),
|
|
/** Completion status */
|
|
completed: int("completed").notNull().default(0),
|
|
/** Optional notes */
|
|
notes: text("notes"),
|
|
/** Pose score if video was analyzed */
|
|
poseScore: float("poseScore"),
|
|
/** Date of training */
|
|
trainingDate: timestamp("trainingDate").defaultNow().notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type TrainingRecord = typeof trainingRecords.$inferSelect;
|
|
export type InsertTrainingRecord = typeof trainingRecords.$inferInsert;
|
|
|
|
/**
|
|
* Live analysis sessions captured from the realtime camera workflow.
|
|
*/
|
|
export const liveAnalysisSessions = mysqlTable("live_analysis_sessions", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
sessionMode: mysqlEnum("sessionMode", ["practice", "pk"]).default("practice").notNull(),
|
|
status: mysqlEnum("status", ["active", "completed", "aborted"]).default("completed").notNull(),
|
|
startedAt: timestamp("startedAt").defaultNow().notNull(),
|
|
endedAt: timestamp("endedAt"),
|
|
durationMs: int("durationMs").default(0).notNull(),
|
|
dominantAction: varchar("dominantAction", { length: 64 }),
|
|
overallScore: float("overallScore"),
|
|
postureScore: float("postureScore"),
|
|
balanceScore: float("balanceScore"),
|
|
techniqueScore: float("techniqueScore"),
|
|
footworkScore: float("footworkScore"),
|
|
consistencyScore: float("consistencyScore"),
|
|
unknownActionRatio: float("unknownActionRatio"),
|
|
totalSegments: int("totalSegments").default(0).notNull(),
|
|
effectiveSegments: int("effectiveSegments").default(0).notNull(),
|
|
totalActionCount: int("totalActionCount").default(0).notNull(),
|
|
videoId: int("videoId"),
|
|
videoUrl: text("videoUrl"),
|
|
summary: text("summary"),
|
|
feedback: json("feedback"),
|
|
metrics: json("metrics"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type LiveAnalysisSession = typeof liveAnalysisSessions.$inferSelect;
|
|
export type InsertLiveAnalysisSession = typeof liveAnalysisSessions.$inferInsert;
|
|
|
|
/**
|
|
* Action segments extracted from a realtime analysis session.
|
|
*/
|
|
export const liveActionSegments = mysqlTable("live_action_segments", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
sessionId: int("sessionId").notNull(),
|
|
actionType: varchar("actionType", { length: 64 }).notNull(),
|
|
isUnknown: int("isUnknown").default(0).notNull(),
|
|
startMs: int("startMs").notNull(),
|
|
endMs: int("endMs").notNull(),
|
|
durationMs: int("durationMs").notNull(),
|
|
confidenceAvg: float("confidenceAvg"),
|
|
score: float("score"),
|
|
peakScore: float("peakScore"),
|
|
frameCount: int("frameCount").default(0).notNull(),
|
|
issueSummary: json("issueSummary"),
|
|
keyFrames: json("keyFrames"),
|
|
clipLabel: varchar("clipLabel", { length: 128 }),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
}, (table) => ({
|
|
sessionIndex: uniqueIndex("live_action_segments_session_start_idx").on(table.sessionId, table.startMs),
|
|
}));
|
|
|
|
export type LiveActionSegment = typeof liveActionSegments.$inferSelect;
|
|
export type InsertLiveActionSegment = typeof liveActionSegments.$inferInsert;
|
|
|
|
/**
|
|
* Daily training aggregate used for streaks, achievements and daily NTRP refresh.
|
|
*/
|
|
export const dailyTrainingAggregates = mysqlTable("daily_training_aggregates", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
dayKey: varchar("dayKey", { length: 32 }).notNull().unique(),
|
|
userId: int("userId").notNull(),
|
|
trainingDate: varchar("trainingDate", { length: 10 }).notNull(),
|
|
totalMinutes: int("totalMinutes").default(0).notNull(),
|
|
sessionCount: int("sessionCount").default(0).notNull(),
|
|
analysisCount: int("analysisCount").default(0).notNull(),
|
|
liveAnalysisCount: int("liveAnalysisCount").default(0).notNull(),
|
|
recordingCount: int("recordingCount").default(0).notNull(),
|
|
pkCount: int("pkCount").default(0).notNull(),
|
|
totalActions: int("totalActions").default(0).notNull(),
|
|
effectiveActions: int("effectiveActions").default(0).notNull(),
|
|
unknownActions: int("unknownActions").default(0).notNull(),
|
|
totalScore: float("totalScore").default(0).notNull(),
|
|
averageScore: float("averageScore").default(0).notNull(),
|
|
metadata: json("metadata"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type DailyTrainingAggregate = typeof dailyTrainingAggregates.$inferSelect;
|
|
export type InsertDailyTrainingAggregate = typeof dailyTrainingAggregates.$inferInsert;
|
|
|
|
/**
|
|
* NTRP Rating history - tracks rating changes over time
|
|
*/
|
|
export const ratingHistory = mysqlTable("rating_history", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
/** NTRP rating at this point */
|
|
rating: float("rating").notNull(),
|
|
/** What triggered this rating update */
|
|
reason: varchar("reason", { length: 256 }),
|
|
/** JSON breakdown of dimension scores */
|
|
dimensionScores: json("dimensionScores"),
|
|
/** Reference analysis ID if applicable */
|
|
analysisId: int("analysisId"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type RatingHistory = typeof ratingHistory.$inferSelect;
|
|
export type InsertRatingHistory = typeof ratingHistory.$inferInsert;
|
|
|
|
/**
|
|
* Daily NTRP snapshots generated by async refresh jobs.
|
|
*/
|
|
export const ntrpSnapshots = mysqlTable("ntrp_snapshots", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
snapshotKey: varchar("snapshotKey", { length: 64 }).notNull().unique(),
|
|
userId: int("userId").notNull(),
|
|
snapshotDate: varchar("snapshotDate", { length: 10 }).notNull(),
|
|
rating: float("rating").notNull(),
|
|
triggerType: mysqlEnum("triggerType", ["analysis", "daily", "manual"]).default("daily").notNull(),
|
|
taskId: varchar("taskId", { length: 64 }),
|
|
dimensionScores: json("dimensionScores"),
|
|
sourceSummary: json("sourceSummary"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type NtrpSnapshot = typeof ntrpSnapshots.$inferSelect;
|
|
export type InsertNtrpSnapshot = typeof ntrpSnapshots.$inferInsert;
|
|
|
|
/**
|
|
* Daily check-in records for streak tracking
|
|
*/
|
|
export const dailyCheckins = mysqlTable("daily_checkins", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
/** Check-in date (YYYY-MM-DD stored as string for easy comparison) */
|
|
checkinDate: varchar("checkinDate", { length: 10 }).notNull(),
|
|
/** Current streak at the time of check-in */
|
|
streakCount: int("streakCount").notNull().default(1),
|
|
/** Optional notes for the day */
|
|
notes: text("notes"),
|
|
/** Training minutes logged this day */
|
|
minutesTrained: int("minutesTrained").default(0),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type DailyCheckin = typeof dailyCheckins.$inferSelect;
|
|
export type InsertDailyCheckin = typeof dailyCheckins.$inferInsert;
|
|
|
|
/**
|
|
* Achievement badges earned by users
|
|
*/
|
|
export const userBadges = mysqlTable("user_badges", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
/** Badge identifier key */
|
|
badgeKey: varchar("badgeKey", { length: 64 }).notNull(),
|
|
/** When the badge was earned */
|
|
earnedAt: timestamp("earnedAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type UserBadge = typeof userBadges.$inferSelect;
|
|
export type InsertUserBadge = typeof userBadges.$inferInsert;
|
|
|
|
/**
|
|
* Achievement definitions that can scale beyond the legacy badge system.
|
|
*/
|
|
export const achievementDefinitions = mysqlTable("achievement_definitions", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
key: varchar("key", { length: 64 }).notNull().unique(),
|
|
name: varchar("name", { length: 128 }).notNull(),
|
|
description: text("description"),
|
|
category: varchar("category", { length: 32 }).notNull(),
|
|
rarity: varchar("rarity", { length: 16 }).default("common").notNull(),
|
|
icon: varchar("icon", { length: 16 }).default("🎾").notNull(),
|
|
metricKey: varchar("metricKey", { length: 64 }).notNull(),
|
|
targetValue: float("targetValue").notNull(),
|
|
tier: int("tier").default(1).notNull(),
|
|
isHidden: int("isHidden").default(0).notNull(),
|
|
isActive: int("isActive").default(1).notNull(),
|
|
sortOrder: int("sortOrder").default(0).notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type AchievementDefinition = typeof achievementDefinitions.$inferSelect;
|
|
export type InsertAchievementDefinition = typeof achievementDefinitions.$inferInsert;
|
|
|
|
/**
|
|
* User achievement progress and unlock records.
|
|
*/
|
|
export const userAchievements = mysqlTable("user_achievements", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
progressKey: varchar("progressKey", { length: 96 }).notNull().unique(),
|
|
userId: int("userId").notNull(),
|
|
achievementKey: varchar("achievementKey", { length: 64 }).notNull(),
|
|
currentValue: float("currentValue").default(0).notNull(),
|
|
progressPct: float("progressPct").default(0).notNull(),
|
|
unlockedAt: timestamp("unlockedAt"),
|
|
lastEvaluatedAt: timestamp("lastEvaluatedAt").defaultNow().notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type UserAchievement = typeof userAchievements.$inferSelect;
|
|
export type InsertUserAchievement = typeof userAchievements.$inferInsert;
|
|
|
|
/**
|
|
* Tutorial video library - professional coaching reference videos
|
|
*/
|
|
export const tutorialVideos = mysqlTable("tutorial_videos", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
category: varchar("category", { length: 64 }).notNull(),
|
|
skillLevel: mysqlEnum("skillLevel", ["beginner", "intermediate", "advanced"]).default("beginner"),
|
|
description: text("description"),
|
|
keyPoints: json("keyPoints"),
|
|
commonMistakes: json("commonMistakes"),
|
|
videoUrl: text("videoUrl"),
|
|
thumbnailUrl: text("thumbnailUrl"),
|
|
duration: int("duration"),
|
|
sortOrder: int("sortOrder").default(0),
|
|
isPublished: int("isPublished").default(1),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type TutorialVideo = typeof tutorialVideos.$inferSelect;
|
|
export type InsertTutorialVideo = typeof tutorialVideos.$inferInsert;
|
|
|
|
/**
|
|
* User tutorial progress tracking
|
|
*/
|
|
export const tutorialProgress = mysqlTable("tutorial_progress", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
tutorialId: int("tutorialId").notNull(),
|
|
watched: int("watched").default(0),
|
|
comparisonVideoId: int("comparisonVideoId"),
|
|
selfScore: float("selfScore"),
|
|
notes: text("notes"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type TutorialProgress = typeof tutorialProgress.$inferSelect;
|
|
export type InsertTutorialProgress = typeof tutorialProgress.$inferInsert;
|
|
|
|
/**
|
|
* Training reminders
|
|
*/
|
|
export const trainingReminders = mysqlTable("training_reminders", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
reminderType: varchar("reminderType", { length: 32 }).notNull(),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
message: text("message"),
|
|
timeOfDay: varchar("timeOfDay", { length: 5 }).notNull(),
|
|
daysOfWeek: json("daysOfWeek").notNull(),
|
|
isActive: int("isActive").default(1),
|
|
lastTriggered: timestamp("lastTriggered"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type TrainingReminder = typeof trainingReminders.$inferSelect;
|
|
export type InsertTrainingReminder = typeof trainingReminders.$inferInsert;
|
|
|
|
/**
|
|
* Notification log
|
|
*/
|
|
export const notificationLog = mysqlTable("notification_log", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
reminderId: int("reminderId"),
|
|
notificationType: varchar("notificationType", { length: 32 }).notNull(),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
message: text("message"),
|
|
isRead: int("isRead").default(0),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type NotificationLogEntry = typeof notificationLog.$inferSelect;
|
|
export type InsertNotificationLog = typeof notificationLog.$inferInsert;
|
|
|
|
/**
|
|
* Background task queue for long-running or retryable work.
|
|
*/
|
|
export const backgroundTasks = mysqlTable("background_tasks", {
|
|
id: varchar("id", { length: 36 }).primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
type: mysqlEnum("type", [
|
|
"media_finalize",
|
|
"training_plan_generate",
|
|
"training_plan_adjust",
|
|
"analysis_corrections",
|
|
"pose_correction_multimodal",
|
|
"ntrp_refresh_user",
|
|
"ntrp_refresh_all",
|
|
]).notNull(),
|
|
status: mysqlEnum("status", ["queued", "running", "succeeded", "failed"]).notNull().default("queued"),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
message: text("message"),
|
|
progress: int("progress").notNull().default(0),
|
|
payload: json("payload").notNull(),
|
|
result: json("result"),
|
|
error: text("error"),
|
|
attempts: int("attempts").notNull().default(0),
|
|
maxAttempts: int("maxAttempts").notNull().default(3),
|
|
workerId: varchar("workerId", { length: 96 }),
|
|
runAfter: timestamp("runAfter").defaultNow().notNull(),
|
|
lockedAt: timestamp("lockedAt"),
|
|
startedAt: timestamp("startedAt"),
|
|
completedAt: timestamp("completedAt"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type BackgroundTask = typeof backgroundTasks.$inferSelect;
|
|
export type InsertBackgroundTask = typeof backgroundTasks.$inferInsert;
|
|
|
|
/**
|
|
* Admin audit trail for privileged actions.
|
|
*/
|
|
export const adminAuditLogs = mysqlTable("admin_audit_logs", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
adminUserId: int("adminUserId").notNull(),
|
|
actionType: varchar("actionType", { length: 64 }).notNull(),
|
|
entityType: varchar("entityType", { length: 64 }).notNull(),
|
|
entityId: varchar("entityId", { length: 96 }),
|
|
targetUserId: int("targetUserId"),
|
|
payload: json("payload"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type AdminAuditLog = typeof adminAuditLogs.$inferSelect;
|
|
export type InsertAdminAuditLog = typeof adminAuditLogs.$inferInsert;
|
|
|
|
/**
|
|
* App settings editable from the admin console.
|
|
*/
|
|
export const appSettings = mysqlTable("app_settings", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
settingKey: varchar("settingKey", { length: 64 }).notNull().unique(),
|
|
label: varchar("label", { length: 128 }).notNull(),
|
|
description: text("description"),
|
|
value: json("value"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type AppSetting = typeof appSettings.$inferSelect;
|
|
export type InsertAppSetting = typeof appSettings.$inferInsert;
|
|
|
|
/**
|
|
* Vision reference library - canonical public tennis images used for multimodal evaluation
|
|
*/
|
|
export const visionReferenceImages = mysqlTable("vision_reference_images", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
slug: varchar("slug", { length: 128 }).notNull().unique(),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
exerciseType: varchar("exerciseType", { length: 64 }).notNull(),
|
|
imageUrl: text("imageUrl").notNull(),
|
|
sourcePageUrl: text("sourcePageUrl").notNull(),
|
|
sourceLabel: varchar("sourceLabel", { length: 128 }).notNull(),
|
|
author: varchar("author", { length: 128 }),
|
|
license: varchar("license", { length: 128 }),
|
|
expectedFocus: json("expectedFocus"),
|
|
tags: json("tags"),
|
|
notes: text("notes"),
|
|
sortOrder: int("sortOrder").default(0).notNull(),
|
|
isPublished: int("isPublished").default(1).notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type VisionReferenceImage = typeof visionReferenceImages.$inferSelect;
|
|
export type InsertVisionReferenceImage = typeof visionReferenceImages.$inferInsert;
|
|
|
|
/**
|
|
* Vision test run history - records each multimodal evaluation against the standard library
|
|
*/
|
|
export const visionTestRuns = mysqlTable("vision_test_runs", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
taskId: varchar("taskId", { length: 64 }).notNull().unique(),
|
|
userId: int("userId").notNull(),
|
|
referenceImageId: int("referenceImageId"),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
exerciseType: varchar("exerciseType", { length: 64 }).notNull(),
|
|
imageUrl: text("imageUrl").notNull(),
|
|
status: mysqlEnum("status", ["queued", "succeeded", "failed"]).default("queued").notNull(),
|
|
visionStatus: mysqlEnum("visionStatus", ["pending", "ok", "fallback", "failed"]).default("pending").notNull(),
|
|
configuredModel: varchar("configuredModel", { length: 128 }),
|
|
expectedFocus: json("expectedFocus"),
|
|
summary: text("summary"),
|
|
corrections: text("corrections"),
|
|
report: json("report"),
|
|
warning: text("warning"),
|
|
error: text("error"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type VisionTestRun = typeof visionTestRuns.$inferSelect;
|
|
export type InsertVisionTestRun = typeof visionTestRuns.$inferInsert;
|