diff --git a/.env.example b/.env.example
index 62503e1..ec89941 100644
--- a/.env.example
+++ b/.env.example
@@ -3,6 +3,7 @@ PORT=3000
# App auth / storage / database
DATABASE_URL=mysql://tennis:replace-with-db-password@db:3306/tennis_training_hub
JWT_SECRET=replace-with-strong-secret
+REGISTRATION_INVITE_CODE=CA2026
VITE_APP_ID=tennis-training-hub
OAUTH_SERVER_URL=
OWNER_OPEN_ID=
diff --git a/README.md b/README.md
index 1682001..3c6dd2f 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@
- 摄像头中断后自动重连,保留既有分段与会话
- Go 媒体 worker 将分段合并归档,并产出 WebM 回放;FFmpeg 可用时额外生成 MP4
- Node app worker 轮询媒体归档状态,归档完成后自动登记到视频库并向任务中心反馈结果
+- 服务端媒体会话校验兼容 `/media/sessions/...` 路径,避免录制结束时因路径不一致导致 404
## Background Tasks
@@ -34,6 +35,8 @@
前端提供全局任务中心,页面本地也会显示任务提交、执行中、完成或失败状态。训练页、分析页和录制页都可以在用户离开页面后继续完成后台任务。
+另外提供独立日志页 `/logs`,用于查看后台任务历史、失败原因与通知记录。
+
## Multimodal LLM
- 文本类任务使用 `LLM_API_URL` / `LLM_API_KEY` / `LLM_MODEL`
@@ -42,6 +45,7 @@
- 若视觉模型链路不可用,系统会自动回退到结构化指标驱动的文本纠正,避免任务直接失败
- 系统内置“视觉标准图库”页面 `/vision-lab`,可把公网网球参考图入库并保存每次识别结果
- `ADMIN_USERNAMES` 可指定哪些用户名账号拥有 admin 视角,例如 `H1`
+- 用户名登录支持直接进入系统;仅首次创建新用户时需要填写 `REGISTRATION_INVITE_CODE`
## Quick Start
@@ -117,6 +121,7 @@ pnpm exec playwright install chromium
- `DATABASE_URL`
- `JWT_SECRET`
- `ADMIN_USERNAMES`
+- `REGISTRATION_INVITE_CODE`
- `MYSQL_DATABASE`
- `MYSQL_USER`
- `MYSQL_PASSWORD`
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 121c2e4..2852451 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -20,6 +20,7 @@ import Recorder from "./pages/Recorder";
import Tutorials from "./pages/Tutorials";
import Reminders from "./pages/Reminders";
import VisionLab from "./pages/VisionLab";
+import Logs from "./pages/Logs";
function DashboardRoute({ component: Component }: { component: React.ComponentType }) {
return (
@@ -70,6 +71,9 @@ function Router() {
+
+
+
diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx
index bcf8ae7..b7d1510 100644
--- a/client/src/components/DashboardLayout.tsx
+++ b/client/src/components/DashboardLayout.tsx
@@ -23,7 +23,7 @@ import { useIsMobile } from "@/hooks/useMobile";
import {
LayoutDashboard, LogOut, PanelLeft, Target, Video,
Award, Activity, FileVideo, Trophy, Flame, Camera, CircleDot,
- BookOpen, Bell, Microscope
+ BookOpen, Bell, Microscope, ScrollText
} from "lucide-react";
import { CSSProperties, useEffect, useRef, useState } from "react";
import { useLocation, Redirect } from "wouter";
@@ -51,6 +51,7 @@ const menuItems: MenuItem[] = [
{ icon: Trophy, label: "排行榜", path: "/leaderboard", group: "stats" },
{ icon: BookOpen, label: "教程库", path: "/tutorials", group: "learn" },
{ icon: Bell, label: "训练提醒", path: "/reminders", group: "learn" },
+ { icon: ScrollText, label: "系统日志", path: "/logs", group: "learn" },
{ icon: Microscope, label: "视觉测试", path: "/vision-lab", group: "learn", adminOnly: true },
];
diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx
index 8042407..891abc3 100644
--- a/client/src/pages/Login.tsx
+++ b/client/src/pages/Login.tsx
@@ -9,6 +9,7 @@ import { Target, Loader2 } from "lucide-react";
export default function Login() {
const [username, setUsername] = useState("");
+ const [inviteCode, setInviteCode] = useState("");
const [, setLocation] = useLocation();
const utils = trpc.useUtils();
const loginMutation = trpc.auth.loginWithUsername.useMutation();
@@ -37,7 +38,10 @@ export default function Login() {
}
try {
- const data = await loginMutation.mutateAsync({ username: username.trim() });
+ const data = await loginMutation.mutateAsync({
+ username: username.trim(),
+ inviteCode: inviteCode.trim() || undefined,
+ });
const user = await syncAuthenticatedUser(data.user);
toast.success(data.isNew ? `已创建用户:${user.name}` : `已登录:${user.name}`);
setLocation("/dashboard");
@@ -77,6 +81,20 @@ export default function Login() {
maxLength={64}
/>
+
+
setInviteCode(e.target.value)}
+ className="h-12 text-base"
+ maxLength={64}
+ />
+
+ 已存在账号只需输入用户名。新用户首次登录需要邀请码。
+
+