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:
ericwyuan
2026-08-23 11:11:29 +08:00
commit d73405decb
34 changed files with 1699 additions and 0 deletions

20
server/.env.example Normal file
View 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
View 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
View 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}`);
});

View 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;
}
}

View 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
View 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;

View 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;

View 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
View 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;
}

View 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
View 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"]
}