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:
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 || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user