feat: DevTools 数据库监看面板 + 隧道控制 + 多项 Bug 修复

**DevTools 新增功能 (Tasks 13-14):**
- 首页仪表盘添加数据库实时监看卡片 (5端口状态 + 记忆数)
- 侧边栏新增数据库面板,支持自动 5 秒刷新
- 数据库面板显示 PostgreSQL/Redis/Qdrant/MinIO/NATS 端口状态
- 隧道控制按钮 (启动/停止/重启/查看状态)
- 新增 API 端点: GET /api/database/status, POST /api/tunnel/:action
- 更新 docs/api-reference/ API 文档

**Bug 修复 (Task 15):**
- 修复 pgrep -f 自匹配导致隧道状态误判 (添加 ^ssh 锚点)
  - devtools/src/index.js (dashboard + database/status)
  - scripts/tunnel.sh (is_tunnel_running + show_status)
- 修复数据库面板缺少自动刷新定时器
- 修复侧边栏数据库徽章永远 display:none
- 修复僵尸进程场景下按钮死锁问题

**其他改进:**
- .gitignore 添加 backend/cmd, backend/iot-debug-service/main
- 前端多项改进 (登录/注册/会话/流式动画等)
This commit is contained in:
2026-05-17 11:42:42 +08:00
parent 0757ad26b5
commit 5d0bb96abe
28 changed files with 1723 additions and 218 deletions
+94 -15
View File
@@ -6,12 +6,16 @@ import { useAuth } from '@/hooks/useAuth';
import { useChat } from '@/hooks/useChat';
export default function App() {
const { isLoggedIn, login, loading: authLoading } = useAuth();
const { isLoggedIn, login, register, loading: authLoading } = useAuth();
const { send } = useChat();
const [authMode, setAuthMode] = useState<'login' | 'register'>('login');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [email, setEmail] = useState('');
const [verifyCode, setVerifyCode] = useState('');
const [error, setError] = useState('');
const [successMsg, setSuccessMsg] = useState('');
const handleLogin = async () => {
setError('');
@@ -21,7 +25,28 @@ export default function App() {
}
};
// 登录页面 (开发阶段暂时禁用注册)
const handleRegister = async () => {
setError('');
setSuccessMsg('');
if (!email) {
setError('请输入邮箱');
return;
}
const result = await register(username, password, email, verifyCode || '000000');
if (!result.success) {
setError(result.error || '注册失败');
} else {
setSuccessMsg('注册成功!正在进入...');
}
};
const switchMode = (mode: 'login' | 'register') => {
setAuthMode(mode);
setError('');
setSuccessMsg('');
};
// 登录/注册页面
if (!isLoggedIn) {
return (
<div className="min-h-screen bg-[#FFFAF5] dark:bg-[#1a1a2e] flex items-center justify-center p-4">
@@ -35,10 +60,30 @@ export default function App() {
</p>
</div>
{/* 登录表单 */}
{/* 登录/注册表单 */}
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-lg p-6 space-y-4 border border-pink-100 dark:border-pink-900">
<div className="text-center mb-2">
<span className="text-sm font-medium text-pink-500"></span>
{/* 模式切换 */}
<div className="flex rounded-xl bg-pink-50 dark:bg-pink-900/20 p-1">
<button
onClick={() => switchMode('login')}
className={`flex-1 py-1.5 text-sm rounded-lg font-medium transition-colors ${
authMode === 'login'
? 'bg-white dark:bg-gray-800 text-pink-500 shadow-sm'
: 'text-gray-400 hover:text-pink-400'
}`}
>
</button>
<button
onClick={() => switchMode('register')}
className={`flex-1 py-1.5 text-sm rounded-lg font-medium transition-colors ${
authMode === 'register'
? 'bg-white dark:bg-gray-800 text-pink-500 shadow-sm'
: 'text-gray-400 hover:text-pink-400'
}`}
>
</button>
</div>
<input
@@ -46,33 +91,67 @@ export default function App() {
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
onKeyDown={(e) => e.key === 'Enter' && (authMode === 'login' ? handleLogin() : handleRegister())}
className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400"
/>
{authMode === 'register' && (
<input
type="email"
placeholder="邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400"
/>
)}
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
onKeyDown={(e) => e.key === 'Enter' && (authMode === 'login' ? handleLogin() : handleRegister())}
className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400"
/>
{authMode === 'register' && (
<input
type="text"
placeholder="验证码 (开发环境输入 000000)"
value={verifyCode}
onChange={(e) => setVerifyCode(e.target.value)}
maxLength={6}
className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400"
/>
)}
{error && (
<p className="text-xs text-red-400 text-center">{error}</p>
)}
{successMsg && (
<p className="text-xs text-green-400 text-center">{successMsg}</p>
)}
<button
onClick={handleLogin}
disabled={authLoading || !username || !password}
className="w-full py-2.5 rounded-xl bg-pink-400 hover:bg-pink-500 disabled:bg-pink-300 text-white font-medium text-sm transition-colors"
>
{authLoading ? '请稍候...' : '进入昔涟的世界 ♪'}
</button>
{authMode === 'login' ? (
<button
onClick={handleLogin}
disabled={authLoading || !username || !password}
className="w-full py-2.5 rounded-xl bg-pink-400 hover:bg-pink-500 disabled:bg-pink-300 text-white font-medium text-sm transition-colors"
>
{authLoading ? '请稍候...' : '进入昔涟的世界 ♪'}
</button>
) : (
<button
onClick={handleRegister}
disabled={authLoading || !username || !password || !email}
className="w-full py-2.5 rounded-xl bg-pink-400 hover:bg-pink-500 disabled:bg-pink-300 text-white font-medium text-sm transition-colors"
>
{authLoading ? '请稍候...' : '注册并进入 ♪'}
</button>
)}
<p className="text-xs text-gray-400 text-center">
· 管理员凭据: admin / cyrene-dev-admin
{authMode === 'register' ? '开发阶段 · 验证码统一使用 000000' : '欢迎回来 ♪'}
</p>
</div>
</div>