feat: add live camera multi-device viewer mode
这个提交包含在:
@@ -16,6 +16,21 @@ export const users = mysqlTable("users", {
|
||||
trainingGoals: text("trainingGoals"),
|
||||
/** NTRP rating (1.0 - 5.0) */
|
||||
ntrpRating: float("ntrpRating").default(1.5),
|
||||
/** Manual NTRP baseline before automated rating is established */
|
||||
manualNtrpRating: float("manualNtrpRating"),
|
||||
manualNtrpCapturedAt: timestamp("manualNtrpCapturedAt"),
|
||||
/** Training assessment profile */
|
||||
heightCm: float("heightCm"),
|
||||
weightKg: float("weightKg"),
|
||||
sprintSpeedScore: int("sprintSpeedScore"),
|
||||
explosivePowerScore: int("explosivePowerScore"),
|
||||
agilityScore: int("agilityScore"),
|
||||
enduranceScore: int("enduranceScore"),
|
||||
flexibilityScore: int("flexibilityScore"),
|
||||
coreStabilityScore: int("coreStabilityScore"),
|
||||
shoulderMobilityScore: int("shoulderMobilityScore"),
|
||||
hipMobilityScore: int("hipMobilityScore"),
|
||||
assessmentNotes: text("assessmentNotes"),
|
||||
/** Total training sessions completed */
|
||||
totalSessions: int("totalSessions").default(0),
|
||||
/** Total training minutes */
|
||||
@@ -215,6 +230,30 @@ export const liveAnalysisSessions = mysqlTable("live_analysis_sessions", {
|
||||
export type LiveAnalysisSession = typeof liveAnalysisSessions.$inferSelect;
|
||||
export type InsertLiveAnalysisSession = typeof liveAnalysisSessions.$inferInsert;
|
||||
|
||||
/**
|
||||
* Per-user runtime state for the current live-camera analysis lock.
|
||||
*/
|
||||
export const liveAnalysisRuntime = mysqlTable("live_analysis_runtime", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: int("userId").notNull(),
|
||||
ownerSid: varchar("ownerSid", { length: 96 }),
|
||||
status: mysqlEnum("status", ["idle", "active", "ended"]).default("idle").notNull(),
|
||||
title: varchar("title", { length: 256 }),
|
||||
sessionMode: mysqlEnum("sessionMode", ["practice", "pk"]).default("practice").notNull(),
|
||||
mediaSessionId: varchar("mediaSessionId", { length: 96 }),
|
||||
startedAt: timestamp("startedAt"),
|
||||
endedAt: timestamp("endedAt"),
|
||||
lastHeartbeatAt: timestamp("lastHeartbeatAt"),
|
||||
snapshot: json("snapshot"),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
userIdUnique: uniqueIndex("live_analysis_runtime_user_idx").on(table.userId),
|
||||
}));
|
||||
|
||||
export type LiveAnalysisRuntime = typeof liveAnalysisRuntime.$inferSelect;
|
||||
export type InsertLiveAnalysisRuntime = typeof liveAnalysisRuntime.$inferInsert;
|
||||
|
||||
/**
|
||||
* Action segments extracted from a realtime analysis session.
|
||||
*/
|
||||
@@ -390,15 +429,34 @@ export type InsertUserAchievement = typeof userAchievements.$inferInsert;
|
||||
*/
|
||||
export const tutorialVideos = mysqlTable("tutorial_videos", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
slug: varchar("slug", { length: 128 }),
|
||||
title: varchar("title", { length: 256 }).notNull(),
|
||||
category: varchar("category", { length: 64 }).notNull(),
|
||||
skillLevel: mysqlEnum("skillLevel", ["beginner", "intermediate", "advanced"]).default("beginner"),
|
||||
topicArea: varchar("topicArea", { length: 32 }).default("tennis_skill"),
|
||||
contentFormat: varchar("contentFormat", { length: 16 }).default("video"),
|
||||
sourcePlatform: varchar("sourcePlatform", { length: 16 }).default("none"),
|
||||
description: text("description"),
|
||||
heroSummary: text("heroSummary"),
|
||||
keyPoints: json("keyPoints"),
|
||||
commonMistakes: json("commonMistakes"),
|
||||
videoUrl: text("videoUrl"),
|
||||
externalUrl: text("externalUrl"),
|
||||
platformVideoId: varchar("platformVideoId", { length: 64 }),
|
||||
thumbnailUrl: text("thumbnailUrl"),
|
||||
duration: int("duration"),
|
||||
estimatedEffortMinutes: int("estimatedEffortMinutes"),
|
||||
prerequisites: json("prerequisites"),
|
||||
learningObjectives: json("learningObjectives"),
|
||||
stepSections: json("stepSections"),
|
||||
deliverables: json("deliverables"),
|
||||
relatedDocPaths: json("relatedDocPaths"),
|
||||
viewCount: int("viewCount"),
|
||||
commentCount: int("commentCount"),
|
||||
metricsFetchedAt: timestamp("metricsFetchedAt"),
|
||||
completionAchievementKey: varchar("completionAchievementKey", { length: 64 }),
|
||||
isFeatured: int("isFeatured").default(0),
|
||||
featuredOrder: int("featuredOrder").default(0),
|
||||
sortOrder: int("sortOrder").default(0),
|
||||
isPublished: int("isPublished").default(1),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
@@ -416,6 +474,8 @@ export const tutorialProgress = mysqlTable("tutorial_progress", {
|
||||
userId: int("userId").notNull(),
|
||||
tutorialId: int("tutorialId").notNull(),
|
||||
watched: int("watched").default(0),
|
||||
completed: int("completed").default(0),
|
||||
completedAt: timestamp("completedAt"),
|
||||
comparisonVideoId: int("comparisonVideoId"),
|
||||
selfScore: float("selfScore"),
|
||||
notes: text("notes"),
|
||||
|
||||
在新工单中引用
屏蔽一个用户