diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..9ee4867 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,24 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + plugins: ['@typescript-eslint', 'react', 'react-hooks'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'prettier' + ], + env: { + browser: true, + node: true, + es2021: true + }, + settings: { + react: { version: 'detect' } + }, + rules: { + 'react/react-in-jsx-scope': 'off' + } +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ab7c517 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Lint + run: npm run lint + continue-on-error: true + + - name: Test + run: npm test + + - name: Build + run: npm run build + + - name: Upload Pages artifact + if: success() + uses: actions/upload-pages-artifact@v1 + with: + path: dist + + - name: Deploy to GitHub Pages + if: success() + uses: actions/deploy-pages@v1 + + - name: Deploy to Vercel + if: success() && env.VERCEL_TOKEN != '' + run: | + npx vercel deploy --prod --token $VERCEL_TOKEN --confirm + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7d2081a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4787e9 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# CodePilot + +## Project Summary + +CodePilot is a comprehensive health management platform that aggregates biometric data, goal tracking, genetic risk insights and personal health events into a single interface. It helps users monitor progress and receive tailored feedback to improve their wellbeing. + +## Key Features + +- **Health tracking** for metrics, symptoms and medications +- **Personalized insights** powered by machine learning +- **Wearable integrations** to sync data from external devices + +## Tech Stack + +- **TypeScript** for client and server code +- **Express** API server +- **PostgreSQL** database (via Drizzle ORM) +- **TailwindCSS** styling +- **Vite** build tool + +## Getting Started + +Install dependencies and launch the development server: + +```bash +npm install +npm run dev +``` + +Use `start-all.sh` to run both the Node.js backend and Streamlit interface together. + +## Folder Structure Overview + +- `client/` – React frontend powered by Vite +- `server/` – Express API and storage modules +- `components/` – Shared UI components for the Streamlit app +- `docs/` – Additional project documentation +- `tests/` – Test suites and helpers +- `utils/` – Utility scripts and shared helpers + +## Contribution Guidelines + +1. Create a topic branch from `main`. +2. Follow the coding style in this repository and add unit tests when possible. +3. Submit changes through a pull request and request review. + +See `docs/BRANCH_PROTECTION.md` for recommended branch protection settings. + +## Future Roadmap + +- Expand wearable device support +- Add advanced analytics and alerting +- Improve CI test coverage + diff --git a/client/src/components/DigitalTwinSimulation.jsx b/client/src/components/DigitalTwinSimulation.jsx index fd5bb02..2f1fe13 100644 --- a/client/src/components/DigitalTwinSimulation.jsx +++ b/client/src/components/DigitalTwinSimulation.jsx @@ -1,16 +1,7 @@ -import React, { useState, lazy, Suspense } from 'react'; +import React, { useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useQuery, useMutation } from "@tanstack/react-query"; -const Line = lazy(() => import('react-chartjs-2').then(m => ({ default: m.Line }))); -import { Loader2 } from 'lucide-react'; - -function ChartFallback() { - return ( -
- -
- ); -} +import { Line } from 'react-chartjs-2'; import { Brain, Play, @@ -630,9 +621,7 @@ function SimulationResults({ results, onClose }) { {/* Chart */}
- }> - - +
{/* Insights */} diff --git a/client/src/components/HealthInsightsCorrelation.jsx b/client/src/components/HealthInsightsCorrelation.jsx index 8ed6001..b47cc7c 100644 --- a/client/src/components/HealthInsightsCorrelation.jsx +++ b/client/src/components/HealthInsightsCorrelation.jsx @@ -1,17 +1,7 @@ -import React, { useState, useEffect, lazy, Suspense } from 'react'; +import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useQuery } from "@tanstack/react-query"; -const Line = lazy(() => import('react-chartjs-2').then(m => ({ default: m.Line }))); -const Scatter = lazy(() => import('react-chartjs-2').then(m => ({ default: m.Scatter }))); -import { Loader2 } from 'lucide-react'; - -function ChartFallback() { - return ( -
- -
- ); -} +import { Line, Scatter } from 'react-chartjs-2'; import { TrendingUp, TrendingDown, @@ -284,8 +274,7 @@ export function HealthInsightsCorrelation() {

Correlation Scatter Plot

- }> - - + />
@@ -331,10 +319,9 @@ export function HealthInsightsCorrelation() {

Timeline Comparison

- }> - + labels: visualizationData.timeline?.metric1Data?.map(d => new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) ) || [], datasets: [ @@ -385,8 +372,7 @@ export function HealthInsightsCorrelation() { } } }} - /> - + />
diff --git a/client/src/components/HealthTimeline.tsx b/client/src/components/HealthTimeline.tsx new file mode 100644 index 0000000..ea239b0 --- /dev/null +++ b/client/src/components/HealthTimeline.tsx @@ -0,0 +1,78 @@ +import { useQuery } from "@tanstack/react-query"; +import { apiRequest } from "@/lib/queryClient"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Badge } from "@/components/ui/badge"; +import { Activity, Target, Dna, CalendarCheck } from "lucide-react"; +import jsPDF from "jspdf"; +import html2canvas from "html2canvas"; +import { useRef } from "react"; + +const iconMap: Record = { + metric: , + goal: , + genetic_risk: , + event: , +}; + +export default function HealthTimeline() { + const containerRef = useRef(null); + const { data: events = [], isLoading } = useQuery({ + queryKey: ["/api/user/timeline"], + queryFn: () => apiRequest("GET", "/api/user/timeline").then(res => res.json()), + }); + + const exportPdf = async () => { + if (!containerRef.current) return; + const canvas = await html2canvas(containerRef.current); + const imgData = canvas.toDataURL("image/png"); + const pdf = new jsPDF("p", "pt", "a4"); + const width = pdf.internal.pageSize.getWidth(); + const height = (canvas.height * width) / canvas.width; + pdf.addImage(imgData, "PNG", 0, 0, width, height); + pdf.save("timeline.pdf"); + }; + + return ( + + + Health Timeline + + + + + {isLoading ? ( +

Loading...

+ ) : ( +
+ {events.map((event: any, idx: number) => ( +
+
+ {iconMap[event.type] || } +
+
+
+

{event.title}

+ + {new Date(event.date).toLocaleDateString()} + +
+ {event.value && ( +

{String(event.value)}

+ )} + + {event.type.replace("_", " ")} + +
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/client/src/components/ProgressDashboard.jsx b/client/src/components/ProgressDashboard.jsx index 1634302..6bb2e33 100644 --- a/client/src/components/ProgressDashboard.jsx +++ b/client/src/components/ProgressDashboard.jsx @@ -1,4 +1,4 @@ -import React, { useState, lazy, Suspense } from 'react'; +import React, { useState } from 'react'; import { motion } from 'framer-motion'; import { useQuery } from "@tanstack/react-query"; import { @@ -27,17 +27,7 @@ import { Progress } from '@/components/ui/progress'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { apiRequest } from '@/lib/queryClient'; -import { Loader2 } from 'lucide-react'; -const Line = lazy(() => import('react-chartjs-2').then(m => ({ default: m.Line }))); -const Bar = lazy(() => import('react-chartjs-2').then(m => ({ default: m.Bar }))); - -function ChartFallback() { - return ( -
- -
- ); -} +import { Line, Bar } from 'react-chartjs-2'; import { Chart as ChartJS, CategoryScale, @@ -325,13 +315,11 @@ function TrendAnalysisView({ trends, timeRange }) { {trends.steps ? ( - }> - - + ) : (
No steps data available for this period @@ -350,13 +338,11 @@ function TrendAnalysisView({ trends, timeRange }) { {trends.sleep ? ( - }> - - + ) : (
No sleep data available for this period @@ -375,13 +361,11 @@ function TrendAnalysisView({ trends, timeRange }) { {trends.hrv ? ( - }> - - + ) : (
No HRV data available for this period @@ -400,13 +384,11 @@ function TrendAnalysisView({ trends, timeRange }) { {trends.activity ? ( - }> - - + ) : (
No activity data available for this period diff --git a/client/src/components/__tests__/health-visualization.test.ts b/client/src/components/__tests__/health-visualization.test.ts deleted file mode 100644 index 2960389..0000000 --- a/client/src/components/__tests__/health-visualization.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { transformHealthData } from '../../lib/transformHealthData'; - -function isoDaysAgo(days: number) { - const d = new Date(); - d.setDate(d.getDate() - days); - return d.toISOString(); -} - -describe('transformHealthData', () => { - it('returns empty array when no stats provided', () => { - expect(transformHealthData([])).toEqual([]); - }); - - it('groups stats by date and parses values', () => { - const day1 = isoDaysAgo(1); - const day2 = isoDaysAgo(2); - const stats = [ - { timestamp: day1, statType: 'heart_rate', value: '80' }, - { timestamp: day1, statType: 'steps', value: '1000' }, - { timestamp: day2, statType: 'sleep', value: '7.5' } - ]; - - const result = transformHealthData(stats); - const date1 = day1.split('T')[0]; - const date2 = day2.split('T')[0]; - const entry1 = result.find(d => d.date === date1); - const entry2 = result.find(d => d.date === date2); - - expect(entry1).toMatchObject({ heartRate: 80, steps: 1000 }); - expect(entry2).toMatchObject({ sleepHours: 7.5 }); - expect(result.length).toBe(7); - }); - - it('handles undefined input', () => { - expect(transformHealthData(undefined as any)).toEqual([]); - }); - - it('ignores unknown stat types', () => { - const day = isoDaysAgo(1); - const stats = [ - { timestamp: day, statType: 'unknown', value: '50' }, - { timestamp: day, statType: 'steps', value: '500' } - ]; - - const result = transformHealthData(stats); - const entry = result.find(d => d.date === day.split('T')[0]); - expect(entry).toMatchObject({ steps: 500 }); - expect((entry as any).unknown).toBeUndefined(); - }); - - it('skips stats with invalid timestamps or values', () => { - const validDay = isoDaysAgo(1); - const stats = [ - { timestamp: 'bad-date', statType: 'heart_rate', value: '80' }, - { timestamp: validDay, statType: 'heart_rate', value: 'abc' }, - { timestamp: validDay, statType: 'heart_rate', value: '70' } - ]; - - const result = transformHealthData(stats); - const entry = result.find(d => d.date === validDay.split('T')[0]); - expect(entry).toMatchObject({ heartRate: 70 }); - }); -}); diff --git a/client/src/components/advanced-trend-analysis.tsx b/client/src/components/advanced-trend-analysis.tsx deleted file mode 100644 index 7e1d652..0000000 --- a/client/src/components/advanced-trend-analysis.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { ResponsiveContainer, LineChart, CartesianGrid, XAxis, YAxis, Tooltip, Line } from 'recharts'; -import type { HealthDataPoint } from '@/lib/transformHealthData'; - -interface Props { - data: HealthDataPoint[]; -} - -export default function AdvancedTrendAnalysis({ data }: Props) { - if (!data.length) return null; - - const movingAverage = data.map((d, idx) => { - const slice = data.slice(Math.max(0, idx - 2), idx + 1); - const hrAvg = slice.reduce((sum, v) => sum + (v.heartRate || 0), 0) / slice.length; - return { date: d.date, hrAvg: parseFloat(hrAvg.toFixed(1)) }; - }); - - return ( -
-

Advanced Trend Analysis

-
- - - - - - - - - -
-
- ); -} diff --git a/client/src/components/connect-health-data.tsx b/client/src/components/connect-health-data.tsx index 381aad0..d1c7c1d 100644 --- a/client/src/components/connect-health-data.tsx +++ b/client/src/components/connect-health-data.tsx @@ -3,97 +3,27 @@ import { useLocation } from "wouter"; import { useToast } from "@/hooks/use-toast"; export default function ConnectHealthDataButton() { -export default function ConnectHealthDataButton() { - const [open, setOpen] = useState(false); - const [selectedProvider, setSelectedProvider] = useState(null); - - function handleConnect(provider: HealthProvider) { - setSelectedProvider(provider); - connectHealthData(provider.id); - } - - function connectHealthData(providerId: string) { - const oauthMap: Record = { - apple: 'apple-health', - google: 'google-fit', - fitbit: 'fitbit', - }; - - const route = oauthMap[providerId]; - if (route) { - window.location.href = `/api/oauth/${route}`; - return; - } - - // Fallback for providers without OAuth yet - alert(`Connecting your health data for personal management from ${providerId}...`); - setTimeout(() => setOpen(false), 1500); - } + const [, navigate] = useLocation(); + const { toast } = useToast(); + + const handleClick = () => { + // Show toast notification + toast({ + title: "Connect Health Data", + description: "Connect your health apps and devices to sync your data", + }); + + // Navigate to the integrations page + navigate("/integrations"); + }; return ( - <> -