Initial commit: Set up Garmin Health Lab project structure
- Initialize monorepo with root workspace configuration - Set up Express.js backend with TypeScript - Set up React 18 frontend with TypeScript - Create database schema with SQLite - Implement project architecture and documentation - Add development and deployment guidelines Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
~*
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# API Keys and secrets
|
||||
.env.production.local
|
||||
config/secrets.json
|
||||
|
||||
# Build artifacts
|
||||
server/dist/
|
||||
client/build/
|
||||
88
CLAUDE.md
Normal file
88
CLAUDE.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Garmin Health Lab - 项目指南
|
||||
|
||||
## 项目简介
|
||||
|
||||
Garmin Health Lab 是一个完整的健康数据分析平台,用于:
|
||||
- 获取和同步佳明(Garmin)设备数据
|
||||
- 进行多维度的健康数据分析
|
||||
- 提供个性化的健康建议
|
||||
- 可视化健康趋势
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端**: React 18 + TypeScript + Recharts
|
||||
- **后端**: Node.js + Express + TypeScript
|
||||
- **数据库**: SQLite3
|
||||
- **API 集成**: Garmin Connect API
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 配置
|
||||
```bash
|
||||
cp server/.env.example server/.env
|
||||
# 编辑 server/.env 填入 Garmin 凭证
|
||||
```
|
||||
|
||||
### 开发
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- 前端: http://localhost:3000
|
||||
- 后端: http://localhost:5000
|
||||
|
||||
## 项目结构
|
||||
|
||||
- `client/` - React 前端应用
|
||||
- `server/` - Node.js 后端服务
|
||||
- `docs/` - 文档
|
||||
|
||||
详见 [ARCHITECTURE.md](docs/ARCHITECTURE.md) 和 [DEVELOPMENT.md](docs/DEVELOPMENT.md)
|
||||
|
||||
## 核心功能
|
||||
|
||||
1. **仪表板** - 健康数据概览和最近数据展示
|
||||
2. **数据同步** - 从 Garmin 同步最新健康数据
|
||||
3. **数据分析** - 趋势分析和数据可视化
|
||||
4. **健康建议** - 基于数据的个性化建议
|
||||
5. **设置** - 用户配置和偏好设置
|
||||
|
||||
## 关键特性
|
||||
|
||||
- ✅ Garmin API 集成
|
||||
- ✅ 多维度数据分析(步数、心率、睡眠等)
|
||||
- ✅ 实时数据同步
|
||||
- ✅ 交互式数据可视化
|
||||
- ✅ JWT 认证安全
|
||||
- ✅ 本地数据存储
|
||||
|
||||
## 开发指南
|
||||
|
||||
- 详见 [DEVELOPMENT.md](docs/DEVELOPMENT.md)
|
||||
- API 文档见 README.md
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
npm run dev # 开发模式
|
||||
npm run build # 构建项目
|
||||
npm run typecheck # 类型检查
|
||||
npm start # 生产模式
|
||||
```
|
||||
|
||||
## 下一步任务
|
||||
|
||||
- [ ] 实现 Garmin OAuth 认证
|
||||
- [ ] 完成 Garmin API 数据获取
|
||||
- [ ] 实现仪表板可视化
|
||||
- [ ] 添加数据分析算法
|
||||
- [ ] 部署和优化
|
||||
|
||||
## 联系方式
|
||||
|
||||
项目维护: ericwyuan.g@gmail.com
|
||||
121
README.md
Normal file
121
README.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 佳明健康数据分析平台 (Garmin Health Lab)
|
||||
|
||||
一个完整的健康数据分析平台,用于获取、分析和可视化你的佳明(Garmin)设备数据。
|
||||
|
||||
## 📊 主要功能
|
||||
|
||||
- **数据同步**: 通过 Garmin API 自动同步你的健康数据
|
||||
- **综合分析**: 步数、心率、睡眠、运动、压力等多维度分析
|
||||
- **数据可视化**: 交互式图表和仪表板展示健康数据趋势
|
||||
- **智能建议**: 基于数据分析的个性化健康建议
|
||||
- **数据导出**: 支持数据导出为 CSV/JSON 格式
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
### 前端
|
||||
- React 18 + TypeScript
|
||||
- Recharts (数据可视化)
|
||||
- Tailwind CSS (样式)
|
||||
- Axios (API 请求)
|
||||
|
||||
### 后端
|
||||
- Node.js + Express
|
||||
- TypeScript
|
||||
- SQLite3 (本地数据存储)
|
||||
- garmin-connect (Garmin API 集成)
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
GarminHealthLab/
|
||||
├── client/ # 前端应用
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React 组件
|
||||
│ │ ├── pages/ # 页面
|
||||
│ │ ├── services/ # API 服务
|
||||
│ │ └── types/ # TypeScript 类型定义
|
||||
│ └── package.json
|
||||
├── server/ # 后端应用
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API 路由
|
||||
│ │ ├── services/ # 业务逻辑
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ ├── middleware/ # 中间件
|
||||
│ │ └── utils/ # 工具函数
|
||||
│ └── package.json
|
||||
├── docs/ # 文档
|
||||
├── package.json # 工作空间根配置
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 前置要求
|
||||
- Node.js 18+
|
||||
- npm 或 yarn
|
||||
- Garmin Connect 账户
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 配置环境变量
|
||||
|
||||
创建 `server/.env` 文件:
|
||||
|
||||
```env
|
||||
PORT=5000
|
||||
NODE_ENV=development
|
||||
GARMIN_CONNECT_USER=your_garmin_email
|
||||
GARMIN_CONNECT_PASSWORD=your_garmin_password
|
||||
DATABASE_PATH=./data/health.db
|
||||
JWT_SECRET=your_jwt_secret_here
|
||||
CORS_ORIGIN=http://localhost:3000
|
||||
```
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- 前端: http://localhost:3000
|
||||
- 后端: http://localhost:5000
|
||||
|
||||
## 📚 API 文档
|
||||
|
||||
### 认证
|
||||
- `POST /api/auth/login` - 用户登录
|
||||
- `POST /api/auth/logout` - 用户登出
|
||||
- `POST /api/auth/refresh` - 刷新 Token
|
||||
|
||||
### Garmin 数据同步
|
||||
- `POST /api/garmin/sync` - 同步 Garmin 数据
|
||||
- `GET /api/garmin/status` - 获取同步状态
|
||||
|
||||
### 健康数据
|
||||
- `GET /api/health/summary` - 获取健康摘要
|
||||
- `GET /api/health/steps` - 获取步数数据
|
||||
- `GET /api/health/heart-rate` - 获取心率数据
|
||||
- `GET /api/health/sleep` - 获取睡眠数据
|
||||
- `GET /api/health/activities` - 获取运动数据
|
||||
|
||||
### 分析与建议
|
||||
- `GET /api/analysis/trends` - 获取数据趋势
|
||||
- `GET /api/analysis/recommendations` - 获取健康建议
|
||||
|
||||
## 🔐 安全说明
|
||||
|
||||
- Garmin 账户密码使用加密存储
|
||||
- 所有 API 请求需要 JWT 认证
|
||||
- 敏感数据不在前端存储
|
||||
|
||||
## 📝 开发指南
|
||||
|
||||
详见 [DEVELOPMENT.md](./docs/DEVELOPMENT.md)
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT
|
||||
44
client/package.json
Normal file
44
client/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "garmin-health-lab-client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:5000",
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"axios": "^1.5.0",
|
||||
"recharts": "^2.8.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"date-fns": "^2.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"typescript": "^5.1.3",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
12
client/public/index.html
Normal file
12
client/public/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="佳明健康数据分析平台">
|
||||
<title>Garmin Health Lab - 佳明健康数据分析</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
26
client/src/App.tsx
Normal file
26
client/src/App.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import Layout from './components/Layout';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import DataSync from './pages/DataSync';
|
||||
import Analysis from './pages/Analysis';
|
||||
import Recommendations from './pages/Recommendations';
|
||||
import Settings from './pages/Settings';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/sync" element={<DataSync />} />
|
||||
<Route path="/analysis" element={<Analysis />} />
|
||||
<Route path="/recommendations" element={<Recommendations />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
129
client/src/components/Layout.css
Normal file
129
client/src/components/Layout.css
Normal file
@@ -0,0 +1,129 @@
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
max-width: 1400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
gap: 2rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.5rem;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: #f0f0ff;
|
||||
border-left-color: #667eea;
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
58
client/src/components/Layout.tsx
Normal file
58
client/src/components/Layout.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import './Layout.css';
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Layout({ children }: LayoutProps) {
|
||||
const location = useLocation();
|
||||
|
||||
const navigationItems = [
|
||||
{ path: '/', label: '仪表板', icon: '📊' },
|
||||
{ path: '/sync', label: '数据同步', icon: '🔄' },
|
||||
{ path: '/analysis', label: '数据分析', icon: '📈' },
|
||||
{ path: '/recommendations', label: '健康建议', icon: '💡' },
|
||||
{ path: '/settings', label: '设置', icon: '⚙️' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<header className="header">
|
||||
<div className="header-content">
|
||||
<h1 className="logo">🏃 Garmin Health Lab</h1>
|
||||
<p className="tagline">佳明健康数据分析平台</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container">
|
||||
<nav className="sidebar">
|
||||
<ul className="nav-list">
|
||||
{navigationItems.map(item => (
|
||||
<li key={item.path}>
|
||||
<Link
|
||||
to={item.path}
|
||||
className={`nav-link ${location.pathname === item.path ? 'active' : ''}`}
|
||||
>
|
||||
<span className="icon">{item.icon}</span>
|
||||
<span className="label">{item.label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main className="content">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer className="footer">
|
||||
<p>© 2024 Garmin Health Lab. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
24
client/src/index.css
Normal file
24
client/src/index.css
Normal file
@@ -0,0 +1,24 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
10
client/src/index.tsx
Normal file
10
client/src/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
13
client/src/pages/Analysis.tsx
Normal file
13
client/src/pages/Analysis.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import './Pages.css';
|
||||
|
||||
function Analysis() {
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>数据分析</h2>
|
||||
<p className="placeholder">数据分析页面即将推出...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Analysis;
|
||||
35
client/src/pages/Dashboard.tsx
Normal file
35
client/src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { apiClient } from '../services/api';
|
||||
import './Pages.css';
|
||||
|
||||
function Dashboard() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [summary, setSummary] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// TODO: Fetch health summary data
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to load summary:', error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div className="page-loading">加载中...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>健康仪表板</h2>
|
||||
<p className="placeholder">仪表板内容即将推出...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
20
client/src/pages/DataSync.tsx
Normal file
20
client/src/pages/DataSync.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import './Pages.css';
|
||||
|
||||
function DataSync() {
|
||||
const handleSync = async () => {
|
||||
// TODO: Implement data sync
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>数据同步</h2>
|
||||
<p className="placeholder">数据同步功能即将推出...</p>
|
||||
<button onClick={handleSync} className="btn btn-primary">
|
||||
同步 Garmin 数据
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataSync;
|
||||
59
client/src/pages/Pages.css
Normal file
59
client/src/pages/Pages.css
Normal file
@@ -0,0 +1,59 @@
|
||||
.page {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.page h2 {
|
||||
color: #333;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.page-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
font-size: 1.2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
font-size: 1.1rem;
|
||||
padding: 2rem;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
13
client/src/pages/Recommendations.tsx
Normal file
13
client/src/pages/Recommendations.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import './Pages.css';
|
||||
|
||||
function Recommendations() {
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>健康建议</h2>
|
||||
<p className="placeholder">健康建议页面即将推出...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Recommendations;
|
||||
13
client/src/pages/Settings.tsx
Normal file
13
client/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import './Pages.css';
|
||||
|
||||
function Settings() {
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>设置</h2>
|
||||
<p className="placeholder">设置页面即将推出...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
78
client/src/services/api.ts
Normal file
78
client/src/services/api.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
|
||||
|
||||
class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Auth endpoints
|
||||
login(email: string, password: string) {
|
||||
return this.client.post('/auth/login', { email, password });
|
||||
}
|
||||
|
||||
logout() {
|
||||
return this.client.post('/auth/logout');
|
||||
}
|
||||
|
||||
// Garmin endpoints
|
||||
syncGarminData() {
|
||||
return this.client.post('/garmin/sync');
|
||||
}
|
||||
|
||||
getGarminSyncStatus() {
|
||||
return this.client.get('/garmin/status');
|
||||
}
|
||||
|
||||
// Health endpoints
|
||||
getHealthSummary(startDate?: string, endDate?: string) {
|
||||
return this.client.get('/health/summary', {
|
||||
params: { startDate, endDate }
|
||||
});
|
||||
}
|
||||
|
||||
getStepsData(startDate?: string, endDate?: string) {
|
||||
return this.client.get('/health/steps', {
|
||||
params: { startDate, endDate }
|
||||
});
|
||||
}
|
||||
|
||||
getHeartRateData(startDate?: string, endDate?: string) {
|
||||
return this.client.get('/health/heart-rate', {
|
||||
params: { startDate, endDate }
|
||||
});
|
||||
}
|
||||
|
||||
getSleepData(startDate?: string, endDate?: string) {
|
||||
return this.client.get('/health/sleep', {
|
||||
params: { startDate, endDate }
|
||||
});
|
||||
}
|
||||
|
||||
getActivities(startDate?: string, endDate?: string) {
|
||||
return this.client.get('/health/activities', {
|
||||
params: { startDate, endDate }
|
||||
});
|
||||
}
|
||||
|
||||
// Analysis endpoints
|
||||
getTrends(metricType?: string) {
|
||||
return this.client.get('/analysis/trends', {
|
||||
params: { metricType }
|
||||
});
|
||||
}
|
||||
|
||||
getRecommendations() {
|
||||
return this.client.get('/analysis/recommendations');
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
37
client/src/types/index.ts
Normal file
37
client/src/types/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface HealthSummary {
|
||||
date: string;
|
||||
steps: number;
|
||||
heartRate?: number;
|
||||
sleep?: {
|
||||
duration: number;
|
||||
quality: number;
|
||||
};
|
||||
caloriesBurned?: number;
|
||||
stress?: number;
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
id: string;
|
||||
activityType: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
duration: number;
|
||||
distance?: number;
|
||||
calories?: number;
|
||||
heartRateAverage?: number;
|
||||
heartRateMax?: number;
|
||||
}
|
||||
|
||||
export interface Recommendation {
|
||||
id: string;
|
||||
category: string;
|
||||
recommendation: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
basedOn: string[];
|
||||
}
|
||||
|
||||
export interface TrendData {
|
||||
date: string;
|
||||
value: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
21
client/tsconfig.json
Normal file
21
client/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
175
docs/ARCHITECTURE.md
Normal file
175
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 项目架构说明
|
||||
|
||||
## 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 浏览器 (React 前端) │
|
||||
│ ┌────────────┬──────────────┬──────────┬──────────┐ │
|
||||
│ │ 仪表板 │ 数据同步 │ 数据分析 │ 设置 │ │
|
||||
│ └────────────┴──────────────┴──────────┴──────────┘ │
|
||||
└──────────────────────────┬──────────────────────────┘
|
||||
│ HTTP/REST
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Node.js/Express 后端服务器 │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ API 路由层 │ │
|
||||
│ │ ├─ /auth - 用户认证 │ │
|
||||
│ │ ├─ /garmin - Garmin 数据同步 │ │
|
||||
│ │ ├─ /health - 健康数据查询 │ │
|
||||
│ │ └─ /analysis - 数据分析 │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ 服务层 │ │
|
||||
│ │ ├─ GarminService - Garmin API 集成 │ │
|
||||
│ │ ├─ HealthService - 健康数据业务逻辑 │ │
|
||||
│ │ ├─ AuthService - 认证授权 │ │
|
||||
│ │ └─ AnalysisService - 数据分析 │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└──────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ SQLite 数据库 │
|
||||
│ ├─ users │
|
||||
│ ├─ health_data │
|
||||
│ ├─ activities │
|
||||
│ └─ sync_status │
|
||||
└──────────────────────────────┘
|
||||
|
||||
|
||||
┌──────────────────────────────┐
|
||||
│ Garmin Cloud API │
|
||||
│ ├─ 用户认证 │
|
||||
│ ├─ 数据获取 │
|
||||
│ └─ 数据同步 │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
## 核心模块
|
||||
|
||||
### 1. 前端 (Client)
|
||||
- **框架**: React 18 + TypeScript
|
||||
- **路由**: React Router
|
||||
- **UI 组件**: 自定义 + CSS
|
||||
- **数据可视化**: Recharts
|
||||
- **API 通信**: Axios
|
||||
|
||||
**主要页面**:
|
||||
- Dashboard (仪表板)
|
||||
- DataSync (数据同步)
|
||||
- Analysis (数据分析)
|
||||
- Recommendations (健康建议)
|
||||
- Settings (设置)
|
||||
|
||||
### 2. 后端 (Server)
|
||||
- **框架**: Express.js
|
||||
- **语言**: TypeScript
|
||||
- **数据库**: SQLite3
|
||||
- **认证**: JWT
|
||||
|
||||
**主要服务**:
|
||||
- **AuthService**: 用户认证和授权
|
||||
- **GarminService**: Garmin API 集成和数据获取
|
||||
- **HealthService**: 健康数据管理
|
||||
- **AnalysisService**: 数据分析和建议生成
|
||||
|
||||
### 3. 数据库 (SQLite)
|
||||
**表结构**:
|
||||
- `users`: 用户信息和 Garmin 凭证
|
||||
- `health_data`: 每日健康数据汇总
|
||||
- `activities`: 运动活动记录
|
||||
- `sync_status`: 数据同步状态追踪
|
||||
|
||||
## 数据流
|
||||
|
||||
### 1. Garmin 数据同步流程
|
||||
```
|
||||
用户点击"同步"
|
||||
↓
|
||||
POST /api/garmin/sync
|
||||
↓
|
||||
GarminService.syncData()
|
||||
↓
|
||||
获取 Garmin 授权 Token
|
||||
↓
|
||||
调用 Garmin API 获取数据
|
||||
↓
|
||||
转换数据格式
|
||||
↓
|
||||
存储到本地 SQLite
|
||||
↓
|
||||
更新 sync_status
|
||||
↓
|
||||
返回同步结果
|
||||
```
|
||||
|
||||
### 2. 数据分析流程
|
||||
```
|
||||
GET /api/health/summary (日期范围)
|
||||
↓
|
||||
从数据库查询健康数据
|
||||
↓
|
||||
计算统计指标
|
||||
↓
|
||||
生成趋势分析
|
||||
↓
|
||||
返回分析结果
|
||||
↓
|
||||
前端绘制图表
|
||||
```
|
||||
|
||||
### 3. 健康建议生成流程
|
||||
```
|
||||
GET /api/analysis/recommendations
|
||||
↓
|
||||
AnalysisService.generateRecommendations()
|
||||
↓
|
||||
分析历史数据
|
||||
↓
|
||||
识别异常和趋势
|
||||
↓
|
||||
根据规则引擎生成建议
|
||||
↓
|
||||
按优先级排序
|
||||
↓
|
||||
返回建议列表
|
||||
```
|
||||
|
||||
## 关键特性
|
||||
|
||||
### 安全性
|
||||
- Garmin 密码使用加密存储
|
||||
- JWT 令牌验证所有请求
|
||||
- CORS 配置限制来源
|
||||
- 环境变量管理敏感配置
|
||||
|
||||
### 扩展性
|
||||
- 模块化的服务设计
|
||||
- 易于添加新的分析算法
|
||||
- 支持数据导出功能
|
||||
- 可扩展的 API 端点
|
||||
|
||||
### 性能
|
||||
- 数据库索引优化查询
|
||||
- API 缓存策略
|
||||
- 异步处理长运行任务
|
||||
- 增量数据同步支持
|
||||
|
||||
## 部署架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Development 本地开发 │
|
||||
│ └─ npm run dev (同时运行前后端) │
|
||||
└─────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────┐
|
||||
│ Production 生产环境 │
|
||||
│ ├─ Docker 容器化 │
|
||||
│ ├─ Nginx 反向代理 │
|
||||
│ ├─ Node.js 后端服务 │
|
||||
│ └─ SQLite 数据持久化 │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
278
docs/DEVELOPMENT.md
Normal file
278
docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# 开发指南
|
||||
|
||||
## 环境设置
|
||||
|
||||
### 前置要求
|
||||
- Node.js 18+
|
||||
- npm 或 yarn
|
||||
- Git
|
||||
- Garmin Connect 账户
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
cd ~/Desktop/Work
|
||||
git clone <repository-url> GarminHealthLab
|
||||
cd GarminHealthLab
|
||||
```
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **配置环境变量**
|
||||
```bash
|
||||
# 复制示例文件
|
||||
cp server/.env.example server/.env
|
||||
|
||||
# 编辑 server/.env,填入你的 Garmin 凭证
|
||||
# GARMIN_CONNECT_USER=your_email@example.com
|
||||
# GARMIN_CONNECT_PASSWORD=your_password
|
||||
# JWT_SECRET=generate_a_random_string
|
||||
```
|
||||
|
||||
4. **启动开发服务器**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- 前端: http://localhost:3000
|
||||
- 后端 API: http://localhost:5000/api
|
||||
|
||||
## 项目命令
|
||||
|
||||
```bash
|
||||
# 开发模式(同时运行前后端)
|
||||
npm run dev
|
||||
|
||||
# 仅运行后端服务器
|
||||
npm run dev:server
|
||||
|
||||
# 仅运行前端应用
|
||||
npm run dev:client
|
||||
|
||||
# 构建项目
|
||||
npm run build
|
||||
|
||||
# 生产模式启动
|
||||
npm start
|
||||
|
||||
# 类型检查
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## 代码结构
|
||||
|
||||
### 后端 (server/)
|
||||
|
||||
```
|
||||
server/
|
||||
├── src/
|
||||
│ ├── index.ts # 入口文件
|
||||
│ ├── routes/ # API 路由
|
||||
│ │ ├── auth.ts
|
||||
│ │ ├── garmin.ts
|
||||
│ │ ├── health.ts
|
||||
│ │ └── analysis.ts
|
||||
│ ├── services/ # 业务逻辑服务
|
||||
│ │ ├── AuthService.ts
|
||||
│ │ ├── GarminService.ts
|
||||
│ │ ├── HealthService.ts
|
||||
│ │ └── AnalysisService.ts
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ └── database.ts
|
||||
│ └── types/ # TypeScript 类型定义
|
||||
├── dist/ # 编译输出
|
||||
├── tsconfig.json
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### 前端 (client/)
|
||||
|
||||
```
|
||||
client/
|
||||
├── src/
|
||||
│ ├── index.tsx # 入口文件
|
||||
│ ├── App.tsx # 主组件
|
||||
│ ├── components/ # 可复用组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── Dashboard.tsx
|
||||
│ │ ├── DataSync.tsx
|
||||
│ │ ├── Analysis.tsx
|
||||
│ │ ├── Recommendations.tsx
|
||||
│ │ └── Settings.tsx
|
||||
│ ├── services/ # API 服务
|
||||
│ │ └── api.ts
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ └── index.css # 全局样式
|
||||
├── public/ # 静态资源
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## 开发工作流
|
||||
|
||||
### 添加新的 API 端点
|
||||
|
||||
1. **创建路由处理器** (`server/src/routes/yourFeature.ts`)
|
||||
```typescript
|
||||
import express from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/endpoint', (req, res) => {
|
||||
// 业务逻辑
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
2. **在主文件中注册路由** (`server/src/index.ts`)
|
||||
```typescript
|
||||
import yourFeatureRoutes from './routes/yourFeature';
|
||||
app.use('/api/yourfeature', yourFeatureRoutes);
|
||||
```
|
||||
|
||||
3. **创建 API 客户端方法** (`client/src/services/api.ts`)
|
||||
```typescript
|
||||
getYourEndpoint() {
|
||||
return this.client.get('/yourfeature/endpoint');
|
||||
}
|
||||
```
|
||||
|
||||
### 添加新页面
|
||||
|
||||
1. **创建页面组件** (`client/src/pages/YourPage.tsx`)
|
||||
```typescript
|
||||
import React from 'react';
|
||||
|
||||
function YourPage() {
|
||||
return <div>Page content</div>;
|
||||
}
|
||||
|
||||
export default YourPage;
|
||||
```
|
||||
|
||||
2. **在 App.tsx 中添加路由**
|
||||
```typescript
|
||||
<Route path="/your-path" element={<YourPage />} />
|
||||
```
|
||||
|
||||
### 数据库操作
|
||||
|
||||
使用数据库工具函数简化操作:
|
||||
|
||||
```typescript
|
||||
import { runAsync, getAsync, allAsync } from '../utils/database';
|
||||
|
||||
// 插入数据
|
||||
await runAsync('INSERT INTO table (col1, col2) VALUES (?, ?)', [val1, val2]);
|
||||
|
||||
// 查询单行
|
||||
const row = await getAsync('SELECT * FROM table WHERE id = ?', [id]);
|
||||
|
||||
// 查询多行
|
||||
const rows = await allAsync('SELECT * FROM table WHERE status = ?', ['active']);
|
||||
```
|
||||
|
||||
## 调试
|
||||
|
||||
### 后端调试
|
||||
```bash
|
||||
# 启用详细日志
|
||||
DEBUG=* npm run dev:server
|
||||
|
||||
# 使用 Node 调试器
|
||||
node --inspect server/dist/index.js
|
||||
```
|
||||
|
||||
### 前端调试
|
||||
- 使用 React DevTools 浏览器扩展
|
||||
- 使用浏览器开发者工具 (F12)
|
||||
- 检查网络请求和响应
|
||||
|
||||
## 测试
|
||||
|
||||
### 测试后端 API
|
||||
|
||||
使用 curl 或 Postman:
|
||||
|
||||
```bash
|
||||
# 获取健康摘要
|
||||
curl http://localhost:5000/api/health/summary
|
||||
|
||||
# 触发 Garmin 同步
|
||||
curl -X POST http://localhost:5000/api/garmin/sync
|
||||
|
||||
# 获取健康建议
|
||||
curl http://localhost:5000/api/analysis/recommendations
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 数据库连接失败
|
||||
```bash
|
||||
# 检查数据库文件
|
||||
ls -la ./data/health.db
|
||||
|
||||
# 重置数据库
|
||||
rm ./data/health.db
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 前端无法连接后端
|
||||
- 检查 CORS 配置
|
||||
- 确保后端运行在 5000 端口
|
||||
- 检查防火墙设置
|
||||
|
||||
### Garmin 认证失败
|
||||
- 验证 Garmin 邮箱和密码
|
||||
- 检查网络连接
|
||||
- 查看后端日志
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
1. **数据库索引**
|
||||
- 在频繁查询的字段上添加索引
|
||||
- 定期分析查询性能
|
||||
|
||||
2. **缓存策略**
|
||||
- 实现 API 响应缓存
|
||||
- 使用浏览器缓存
|
||||
|
||||
3. **代码分割**
|
||||
- React 懒加载路由
|
||||
- 按需加载 JavaScript
|
||||
|
||||
## 代码规范
|
||||
|
||||
### TypeScript
|
||||
- 使用严格模式
|
||||
- 为所有函数参数添加类型
|
||||
- 使用接口定义复杂对象
|
||||
|
||||
### CSS
|
||||
- 使用 BEM 命名规范
|
||||
- 响应式设计优先
|
||||
- 避免内联样式
|
||||
|
||||
### 提交信息
|
||||
```
|
||||
feat: 添加新功能描述
|
||||
fix: 修复 bug 描述
|
||||
docs: 文档更新
|
||||
style: 代码格式调整
|
||||
refactor: 代码重构
|
||||
test: 测试相关
|
||||
```
|
||||
|
||||
## 资源链接
|
||||
|
||||
- [Express.js 文档](https://expressjs.com/)
|
||||
- [React 文档](https://react.dev/)
|
||||
- [TypeScript 文档](https://www.typescriptlang.org/)
|
||||
- [SQLite 文档](https://www.sqlite.org/)
|
||||
- [Garmin API](https://developer.garmin.com/)
|
||||
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "garmin-health-lab",
|
||||
"version": "0.1.0",
|
||||
"description": "佳明健康数据分析平台",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
||||
"dev:server": "cd server && npm run dev",
|
||||
"dev:client": "cd client && npm start",
|
||||
"build": "npm run build:server && npm run build:client",
|
||||
"build:server": "cd server && npm run build",
|
||||
"build:client": "cd client && npm run build",
|
||||
"start": "node server/dist/index.js"
|
||||
},
|
||||
"workspaces": [
|
||||
"server",
|
||||
"client"
|
||||
],
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.0"
|
||||
}
|
||||
}
|
||||
20
server/.env.example
Normal file
20
server/.env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# Server Configuration
|
||||
PORT=5000
|
||||
NODE_ENV=development
|
||||
|
||||
# Database
|
||||
DATABASE_PATH=./data/health.db
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_jwt_secret_key_here_change_in_production
|
||||
|
||||
# CORS
|
||||
CORS_ORIGIN=http://localhost:3000
|
||||
|
||||
# Garmin Configuration
|
||||
# Note: Consider using OAuth instead of storing credentials
|
||||
GARMIN_CONNECT_USER=your_garmin_email@example.com
|
||||
GARMIN_CONNECT_PASSWORD=your_password_here
|
||||
|
||||
# Data Sync
|
||||
SYNC_INTERVAL_HOURS=1
|
||||
29
server/package.json
Normal file
29
server/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "garmin-health-lab-server",
|
||||
"version": "0.1.0",
|
||||
"description": "Garmin Health Lab Backend",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext .ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"axios": "^1.5.0",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"typescript": "^5.1.3",
|
||||
"tsx": "^3.12.7"
|
||||
}
|
||||
}
|
||||
42
server/src/index.ts
Normal file
42
server/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import { initializeDatabase } from './utils/database';
|
||||
import authRoutes from './routes/auth';
|
||||
import garminRoutes from './routes/garmin';
|
||||
import healthRoutes from './routes/health';
|
||||
import analysisRoutes from './routes/analysis';
|
||||
import { errorHandler } from './middleware/errorHandler';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Initialize database
|
||||
initializeDatabase();
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/garmin', garminRoutes);
|
||||
app.use('/api/health', healthRoutes);
|
||||
app.use('/api/analysis', analysisRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health/status', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
31
server/src/middleware/errorHandler.ts
Normal file
31
server/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export function errorHandler(
|
||||
err: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
console.error('Error:', err);
|
||||
|
||||
const status = err.status || 500;
|
||||
const message = err.message || 'Internal server error';
|
||||
|
||||
res.status(status).json({
|
||||
error: {
|
||||
status,
|
||||
message,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public message: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
14
server/src/routes/analysis.ts
Normal file
14
server/src/routes/analysis.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import express from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// TODO: Implement analysis endpoints
|
||||
router.get('/trends', (req, res) => {
|
||||
res.json({ message: 'Trends analysis endpoint' });
|
||||
});
|
||||
|
||||
router.get('/recommendations', (req, res) => {
|
||||
res.json({ message: 'Recommendations endpoint' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
18
server/src/routes/auth.ts
Normal file
18
server/src/routes/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import express from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// TODO: Implement authentication routes
|
||||
router.post('/login', (req, res) => {
|
||||
res.json({ message: 'Login endpoint' });
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
res.json({ message: 'Logout endpoint' });
|
||||
});
|
||||
|
||||
router.post('/refresh', (req, res) => {
|
||||
res.json({ message: 'Refresh token endpoint' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
14
server/src/routes/garmin.ts
Normal file
14
server/src/routes/garmin.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import express from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// TODO: Implement Garmin API integration
|
||||
router.post('/sync', (req, res) => {
|
||||
res.json({ message: 'Garmin sync endpoint' });
|
||||
});
|
||||
|
||||
router.get('/status', (req, res) => {
|
||||
res.json({ message: 'Garmin sync status endpoint' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
26
server/src/routes/health.ts
Normal file
26
server/src/routes/health.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import express from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// TODO: Implement health data endpoints
|
||||
router.get('/summary', (req, res) => {
|
||||
res.json({ message: 'Health summary endpoint' });
|
||||
});
|
||||
|
||||
router.get('/steps', (req, res) => {
|
||||
res.json({ message: 'Steps data endpoint' });
|
||||
});
|
||||
|
||||
router.get('/heart-rate', (req, res) => {
|
||||
res.json({ message: 'Heart rate data endpoint' });
|
||||
});
|
||||
|
||||
router.get('/sleep', (req, res) => {
|
||||
res.json({ message: 'Sleep data endpoint' });
|
||||
});
|
||||
|
||||
router.get('/activities', (req, res) => {
|
||||
res.json({ message: 'Activities endpoint' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
59
server/src/types/index.ts
Normal file
59
server/src/types/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
garminEmail: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface HealthData {
|
||||
id: string;
|
||||
userId: string;
|
||||
date: Date;
|
||||
steps: number;
|
||||
heartRate?: number;
|
||||
heartRateVariability?: number;
|
||||
bloodPressure?: {
|
||||
systolic: number;
|
||||
diastolic: number;
|
||||
};
|
||||
sleep?: {
|
||||
duration: number;
|
||||
quality: number;
|
||||
};
|
||||
stress?: number;
|
||||
caloriesBurned?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
id: string;
|
||||
userId: string;
|
||||
activityType: string;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
duration: number;
|
||||
distance?: number;
|
||||
calories?: number;
|
||||
heartRateAverage?: number;
|
||||
heartRateMax?: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface HealthRecommendation {
|
||||
id: string;
|
||||
userId: string;
|
||||
category: string;
|
||||
recommendation: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
basedOn: string[];
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface SyncStatus {
|
||||
lastSyncTime: Date;
|
||||
status: 'idle' | 'syncing' | 'error';
|
||||
lastError?: string;
|
||||
recordsSynced: number;
|
||||
}
|
||||
112
server/src/utils/database.ts
Normal file
112
server/src/utils/database.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import sqlite3 from 'sqlite3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const dbPath = process.env.DATABASE_PATH || './data/health.db';
|
||||
|
||||
// Ensure data directory exists
|
||||
const dataDir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
export const db = new sqlite3.Database(dbPath);
|
||||
|
||||
export function initializeDatabase() {
|
||||
db.serialize(() => {
|
||||
// Users table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
garmin_email TEXT NOT NULL,
|
||||
garmin_password_hash TEXT NOT NULL,
|
||||
jwt_token TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Health data table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS health_data (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
steps INTEGER,
|
||||
heart_rate INTEGER,
|
||||
heart_rate_variability REAL,
|
||||
blood_pressure_systolic INTEGER,
|
||||
blood_pressure_diastolic INTEGER,
|
||||
sleep_duration INTEGER,
|
||||
sleep_quality REAL,
|
||||
stress INTEGER,
|
||||
calories_burned REAL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
UNIQUE(user_id, date)
|
||||
)
|
||||
`);
|
||||
|
||||
// Activities table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS activities (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
activity_type TEXT NOT NULL,
|
||||
start_time DATETIME NOT NULL,
|
||||
end_time DATETIME NOT NULL,
|
||||
duration INTEGER,
|
||||
distance REAL,
|
||||
calories REAL,
|
||||
heart_rate_average INTEGER,
|
||||
heart_rate_max INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Sync status table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS sync_status (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
last_sync_time DATETIME,
|
||||
status TEXT DEFAULT 'idle',
|
||||
last_error TEXT,
|
||||
records_synced INTEGER DEFAULT 0,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
console.log('Database initialized successfully');
|
||||
});
|
||||
}
|
||||
|
||||
export function runAsync(sql: string, params: any[] = []): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(sql, params, function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID, changes: this.changes });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getAsync(sql: string, params: any[] = []): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(sql, params, (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function allAsync(sql: string, params: any[] = []): Promise<any[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(sql, params, (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
20
server/tsconfig.json
Normal file
20
server/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user