Przeglądaj źródła

feat: wire Apple Health data to frontend - recovery score banner, real sleep, HRV on monitoring

17846405080 1 miesiąc temu
rodzic
commit
a1aa892918

+ 2 - 1
campus_health_app/backend/src/config/database.ts

@@ -7,6 +7,7 @@ import { WeightGoal } from '../entities/WeightGoal';
 import { Tag } from '../entities/Tag';
 import { WeightRecordTag } from '../entities/WeightRecordTag';
 import { AnomalyLog } from '../entities/AnomalyLog';
+import { DailyHealthMetric } from '../entities/DailyHealthMetric';
 
 dotenv.config();
 
@@ -24,7 +25,7 @@ const databaseOptions: DataSourceOptions = {
   username: process.env.DB_USERNAME || 'root',
   password: process.env.DB_PASSWORD || '',
   database: process.env.DB_DATABASE || 'campus_health',
-  entities: [User, WeightRecord, WeightGoal, Tag, WeightRecordTag, AnomalyLog],
+  entities: [User, WeightRecord, WeightGoal, Tag, WeightRecordTag, AnomalyLog, DailyHealthMetric],
   synchronize: parseBoolean(process.env.DB_SYNCHRONIZE, false),
   logging: parseBoolean(process.env.DB_LOGGING, false),
   timezone: process.env.DB_TIMEZONE || '+08:00',

+ 43 - 0
campus_health_app/backend/src/controllers/HealthMetricController.ts

@@ -0,0 +1,43 @@
+import { Request, Response } from 'express';
+import { HealthMetricService } from '../services/HealthMetricService';
+
+export class HealthMetricController {
+  private service = new HealthMetricService();
+
+  getRecent = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const days = Math.min(Number(req.query.days) || 30, 365);
+      const data = await this.service.getRecent(days);
+      res.json({ success: true, data });
+    } catch (e: any) {
+      res.status(500).json({ success: false, message: e.message });
+    }
+  };
+
+  getLatest = async (_req: Request, res: Response): Promise<void> => {
+    try {
+      const data = await this.service.getLatest();
+      res.json({ success: true, data });
+    } catch (e: any) {
+      res.status(500).json({ success: false, message: e.message });
+    }
+  };
+
+  getRecovery = async (_req: Request, res: Response): Promise<void> => {
+    try {
+      const data = await this.service.getRecovery();
+      res.json({ success: true, data });
+    } catch (e: any) {
+      res.status(500).json({ success: false, message: e.message });
+    }
+  };
+
+  getMenstrualProfile = async (_req: Request, res: Response): Promise<void> => {
+    try {
+      const data = await this.service.getMenstrualProfile();
+      res.json({ success: true, data });
+    } catch (e: any) {
+      res.status(500).json({ success: false, message: e.message });
+    }
+  };
+}

+ 73 - 0
campus_health_app/backend/src/entities/DailyHealthMetric.ts

@@ -0,0 +1,73 @@
+import { Entity, PrimaryColumn, Column } from 'typeorm';
+
+@Entity('daily_health_metrics')
+export class DailyHealthMetric {
+  @PrimaryColumn('date', { name: 'metric_date' })
+  metricDate!: string;
+
+  @Column('int', { name: 'step_count', default: 0 })
+  stepCount!: number;
+
+  @Column('decimal', { name: 'active_energy_kcal', precision: 8, scale: 2, default: 0 })
+  activeEnergyKcal!: number;
+
+  @Column('decimal', { name: 'basal_energy_kcal', precision: 8, scale: 2, default: 0 })
+  basalEnergyKcal!: number;
+
+  @Column('int', { name: 'exercise_minutes', default: 0 })
+  exerciseMinutes!: number;
+
+  @Column('int', { name: 'workout_count', default: 0 })
+  workoutCount!: number;
+
+  @Column('varchar', { name: 'workout_types', length: 255, nullable: true })
+  workoutTypes!: string | null;
+
+  @Column('decimal', { name: 'resting_heart_rate', precision: 5, scale: 1, nullable: true })
+  restingHeartRate!: number | null;
+
+  @Column('decimal', { name: 'hrv_sdnn', precision: 6, scale: 1, nullable: true })
+  hrvSdnn!: number | null;
+
+  @Column('decimal', { name: 'avg_heart_rate', precision: 5, scale: 1, nullable: true })
+  avgHeartRate!: number | null;
+
+  @Column('decimal', { name: 'avg_oxygen_saturation', precision: 5, scale: 2, nullable: true })
+  avgOxygenSaturation!: number | null;
+
+  @Column('decimal', { name: 'avg_respiratory_rate', precision: 5, scale: 1, nullable: true })
+  avgRespiratoryRate!: number | null;
+
+  @Column('decimal', { name: 'sleep_duration_hours', precision: 4, scale: 2, default: 0 })
+  sleepDurationHours!: number;
+
+  @Column('decimal', { name: 'sleep_in_bed_hours', precision: 4, scale: 2, default: 0 })
+  sleepInBedHours!: number;
+
+  @Column('decimal', { name: 'deep_sleep_hours', precision: 4, scale: 2, default: 0 })
+  deepSleepHours!: number;
+
+  @Column('decimal', { name: 'rem_sleep_hours', precision: 4, scale: 2, default: 0 })
+  remSleepHours!: number;
+
+  @Column('decimal', { name: 'core_sleep_hours', precision: 4, scale: 2, default: 0 })
+  coreSleepHours!: number;
+
+  @Column('decimal', { name: 'wrist_temp_c', precision: 4, scale: 2, nullable: true })
+  wristTempC!: number | null;
+
+  @Column('tinyint', { name: 'is_menstruating', default: 0 })
+  isMenstruating!: boolean;
+
+  @Column('varchar', { name: 'menstrual_flow', length: 30, nullable: true })
+  menstrualFlow!: string | null;
+
+  @Column('tinyint', { name: 'fatigue_reported', default: 0 })
+  fatigueReported!: boolean;
+
+  @Column('tinyint', { name: 'bloating_reported', default: 0 })
+  bloatingReported!: boolean;
+
+  @Column('tinyint', { name: 'mood_changes', default: 0 })
+  moodChanges!: boolean;
+}

+ 15 - 0
campus_health_app/backend/src/routes/health-metric.routes.ts

@@ -0,0 +1,15 @@
+import { Router } from 'express';
+import { HealthMetricController } from '../controllers/HealthMetricController';
+import { authMiddleware } from '../middlewares/auth.middleware';
+
+const router = Router();
+const c = new HealthMetricController();
+
+router.use(authMiddleware);
+
+router.get('/recent', c.getRecent);
+router.get('/latest', c.getLatest);
+router.get('/recovery', c.getRecovery);
+router.get('/menstrual-profile', c.getMenstrualProfile);
+
+export default router;

+ 3 - 21
campus_health_app/backend/src/routes/index.ts

@@ -1,10 +1,13 @@
 import { Router } from 'express';
 import weightRoutes from './weight.routes';
+import healthMetricRoutes from './health-metric.routes';
 
 const router = Router();
 
 // 挂载体重管理路由
 router.use('/weight', weightRoutes);
+// 健康指标(Apple Health ETL 数据)
+router.use('/health-metrics', healthMetricRoutes);
 
 // 健康检查路由
 router.get('/health', (_req, res) => {
@@ -16,24 +19,3 @@ router.get('/health', (_req, res) => {
 });
 
 export default router;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

+ 114 - 0
campus_health_app/backend/src/services/HealthMetricService.ts

@@ -0,0 +1,114 @@
+import { AppDataSource } from '../config/database';
+import { DailyHealthMetric } from '../entities/DailyHealthMetric';
+import { Between, IsNull, Not } from 'typeorm';
+
+export class HealthMetricService {
+  private repo = AppDataSource.getRepository(DailyHealthMetric);
+
+  /** 最近 N 天的每日指标 */
+  async getRecent(days: number) {
+    const since = new Date();
+    since.setDate(since.getDate() - days);
+    const sinceStr = since.toISOString().slice(0, 10);
+    return this.repository.find({
+      where: { metricDate: Between(sinceStr, new Date().toISOString().slice(0, 10)) },
+      order: { metricDate: 'ASC' }
+    });
+  }
+
+  /** 最新一天(dashboard 用) */
+  async getLatest() {
+    const row = await this.repository.findOne({
+      where: { hrvSdnn: Not(IsNull()) },
+      order: { metricDate: 'DESC' }
+    });
+    return row;
+  }
+
+  /**
+   * 恢复分:基于 HRV 相对 30 天基线的比值
+   * >1.0 状态好 / 0.85-1.0 一般 / <0.85 疲劳
+   */
+  async getRecovery() {
+    const rows = await this.repository.find({
+      where: { hrvSdnn: Not(IsNull()) },
+      order: { metricDate: 'DESC' },
+      take: 30
+    });
+    if (rows.length < 7) {
+      return { enough: false, days: rows.length };
+    }
+    const baseline = rows.reduce((s, r) => s + Number(r.hrvSdnn), 0) / rows.length;
+    const today = rows[0];
+    const todayHrv = Number(today.hrvSdnn);
+    const ratio = todayHrv / baseline;
+    let status: 'good' | 'ok' | 'fatigued' = 'ok';
+    if (ratio >= 1.0) status = 'good';
+    else if (ratio < 0.85) status = 'fatigued';
+
+    return {
+      enough: true,
+      today: {
+        date: today.metricDate,
+        hrv: todayHrv,
+        restingHr: today.restingHeartRate,
+        sleepHours: today.sleepDurationHours,
+        deepSleepHours: today.deepSleepHours,
+      },
+      baseline30d: Math.round(baseline * 10) / 10,
+      ratio: Math.round(ratio * 100) / 100,
+      status,
+    };
+  }
+
+  /** 经期 profile:最近 6 次经期前后 7 天的均值 */
+  async getMenstrualProfile() {
+    // 查所有行按时间排序,找经期跳变
+    const rows = await this.repository.find({
+      select: ['metricDate', 'isMenstruating', 'restingHeartRate', 'hrvSdnn', 'sleepDurationHours', 'workoutCount'],
+      order: { metricDate: 'ASC' }
+    });
+    const starts: string[] = [];
+    let prev = false;
+    for (const r of rows) {
+      const cur = !!r.isMenstruating;
+      if (cur && !prev) starts.push(r.metricDate);
+      prev = cur;
+    }
+    // 最近 6 次
+    const recentStarts = starts.slice(-6);
+    if (recentStarts.length === 0) return { periods: 0, profile: [] };
+
+    // 对每次经期,聚合 offset -7..+7 的平均指标
+    const byOffset: Record<number, { hr: number[]; hrv: number[]; sleep: number[]; workouts: number }> = {};
+    for (const start of recentStarts) {
+      const startDate = new Date(start);
+      for (let off = -7; off <= 7; off++) {
+        const d = new Date(startDate);
+        d.setDate(d.getDate() + off);
+        const dStr = d.toISOString().slice(0, 10);
+        const row = await this.repository.findOneBy({ metricDate: dStr });
+        if (!row) continue;
+        if (!byOffset[off]) byOffset[off] = { hr: [], hrv: [], sleep: [], workouts: 0 };
+        if (row.restingHeartRate) byOffset[off].hr.push(Number(row.restingHeartRate));
+        if (row.hrvSdnn) byOffset[off].hrv.push(Number(row.hrvSdnn));
+        if (row.sleepDurationHours) byOffset[off].sleep.push(Number(row.sleepDurationHours));
+        byOffset[off].workouts += row.workoutCount;
+      }
+    }
+
+    const profile = Object.entries(byOffset).map(([offset, v]) => ({
+      offset: Number(offset),
+      avgRestingHr: v.hr.length ? Math.round(v.hr.reduce((a,b)=>a+b,0)/v.hr.length * 10) / 10 : null,
+      avgHrv: v.hrv.length ? Math.round(v.hrv.reduce((a,b)=>a+b,0)/v.hrv.length * 10) / 10 : null,
+      avgSleepHours: v.sleep.length ? Math.round(v.sleep.reduce((a,b)=>a+b,0)/v.sleep.length * 100) / 100 : null,
+      totalWorkouts: v.workouts,
+      samples: Math.min(v.hr.length, v.hrv.length, v.sleep.length),
+    }));
+    return { periods: recentStarts.length, profile };
+  }
+
+  private get repository() {
+    return this.repo;
+  }
+}

+ 8 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/dashboard/dashboard.component.html

@@ -30,6 +30,14 @@
             <span>今天感觉如何?</span>
           </p>
           <p class="home-sub">下面是你今天最重要的三个数字:体重 · 运动 · 饮食</p>
+          <div class="recovery-banner" *ngIf="recovery" style="margin-top:8px; padding:8px 12px; border-radius:8px; font-size:13px; display:inline-flex; align-items:center; gap:8px;"
+               [style.background]="recovery.status === 'good' ? '#e8f5e9' : recovery.status === 'fatigued' ? '#ffebee' : '#fff8e1'"
+               [style.color]="recovery.status === 'good' ? '#2e7d32' : recovery.status === 'fatigued' ? '#c62828' : '#f57f17'">
+            <span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:currentColor"></span>
+            <span *ngIf="recovery.status === 'good'">✓ 恢复良好:HRV {{ recovery.hrv }}ms(比 30 天均值高 {{ (recovery.ratio*100 - 100).toFixed(0) }}%)</span>
+            <span *ngIf="recovery.status === 'ok'">恢复一般:HRV {{ recovery.hrv }}ms,30 天均值 {{ recovery.baseline }}ms</span>
+            <span *ngIf="recovery.status === 'fatigued'">⚠️ 身体疲劳:HRV {{ recovery.hrv }}ms(低于 30 天均值),建议低强度训练或休息</span>
+          </div>
         </div>
         <div class="home-avatar" (click)="openAiChatModal()" style="cursor: pointer; padding: 0;">
           <img src="assets/images/black-cat.png" alt="AI Chat" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;">

+ 45 - 2
campus_health_app/frontend/campus-health-app/src/app/modules/dashboard/dashboard.component.ts

@@ -1,6 +1,7 @@
 import { Component } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { Router, RouterModule } from '@angular/router';
+import { HttpClient } from '@angular/common/http';
 import { BaseChartDirective } from 'ng2-charts';
 import { ChartConfiguration, ChartType, ChartData, Chart } from 'chart.js';
 import 'chart.js/auto';
@@ -273,12 +274,54 @@ export class DashboardComponent {
   // 月度睡眠数据 (模拟)
   monthlySleepData: WeeklySleepData[] = [];
 
-  constructor(private router: Router) {
+  recovery: { status: string; ratio: number; baseline: number; hrv: number; restingHr: number } | null = null;
+
+  constructor(private router: Router, private http: HttpClient) {
     this.initMonthlyData();
-    // 初始化图表为日视图
     this.updateSleepChart();
   }
 
+  ngOnInit() {
+    this.loadHealthMetrics();
+  }
+
+  private loadHealthMetrics() {
+    // 最新一天睡眠 + 恢复分
+    this.http.get<any>('/api/health-metrics/latest').subscribe({
+      next: (res) => {
+        if (res.success && res.data) {
+          const d = res.data;
+          const total = Number(d.sleepDurationHours) || 0;
+          const hours = Math.floor(total);
+          const minutes = Math.round((total - hours) * 60);
+          this.sleepCardData.sleepHours = hours;
+          this.sleepCardData.sleepMinutes = minutes;
+          this.sleepCardData.deepSleepDuration = Number(d.deepSleepHours) || 0;
+          this.sleepCardData.sleepStatus = total >= 7 ? '良好' : total >= 6 ? '一般' : '偏少';
+          this.sleepCardData.sleepScore = Math.min(100, Math.round(total / 9 * 100));
+          // 详情弹窗
+          this.sleepData.totalSleepTime = total;
+          this.sleepData.deepSleepDuration = Number(d.deepSleepHours) || 0;
+          this.sleepData.remSleepDuration = Number(d.remSleepHours) || 0;
+          this.sleepData.lightSleepDuration = Number(d.coreSleepHours) || 0;
+        }
+      }
+    });
+    this.http.get<any>('/api/health-metrics/recovery').subscribe({
+      next: (res) => {
+        if (res.success && res.data && res.data.enough) {
+          this.recovery = {
+            status: res.data.status,
+            ratio: res.data.ratio,
+            baseline: res.data.baseline30d,
+            hrv: res.data.today.hrv,
+            restingHr: res.data.today.restingHr,
+          };
+        }
+      }
+    });
+  }
+
   private initMonthlyData() {
     // 生成30天模拟数据
     for (let i = 1; i <= 30; i++) {

+ 20 - 6
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.html

@@ -23,19 +23,33 @@
       </div>
     </section>
 
+    <!-- 恢复分横幅 -->
+    <section *ngIf="recovery" style="margin-bottom:16px; padding:14px 16px; border-radius:12px;"
+             [style.background]="recovery.status === 'good' ? '#e8f5e9' : recovery.status === 'fatigued' ? '#ffebee' : '#fff8e1'"
+             [style.color]="recovery.status === 'good' ? '#2e7d32' : recovery.status === 'fatigued' ? '#c62828' : '#f57f17'">
+      <div style="font-weight:600; margin-bottom:4px;">
+        {{ recovery.status === 'good' ? '✓ 恢复状态良好' : recovery.status === 'fatigued' ? '⚠️ 身体疲劳预警' : '恢复一般' }}
+      </div>
+      <div style="font-size:13px; opacity:0.9;">
+        今日 HRV {{ recovery.hrv }}ms · 30 天均值 {{ recovery.baseline }}ms · 比值 {{ recovery.ratio }}
+        <span *ngIf="recovery.status === 'good'"> —— 可以正常训练</span>
+        <span *ngIf="recovery.status === 'fatigued'"> —— 建议低强度或休息</span>
+      </div>
+    </section>
+
     <!-- 核心指标网格 -->
     <section class="metrics-grid">
       <div class="metric-item" (click)="openMetricModal('heartRate')">
         <div class="metric-icon">💗</div>
         <div class="metric-value">{{ heartRate.value }}<span class="metric-unit">{{ heartRate.unit }}</span></div>
-        <div class="metric-label">心率</div>
+        <div class="metric-label">静息心率</div>
         <span class="metric-status" [class.status-normal]="heartRate.status === 'normal'" [class.status-warning]="heartRate.status === 'warning'" [class.status-danger]="heartRate.status === 'danger'">{{ getStatusText(heartRate.status) }}</span>
       </div>
-      <div class="metric-item" (click)="openMetricModal('bloodPressure')">
-        <div class="metric-icon">🩺</div>
-        <div class="metric-value">{{ bloodPressure.systolic.value }}<span class="metric-unit">/{{ bloodPressure.diastolic.value }}</span></div>
-        <div class="metric-label">血压</div>
-        <span class="metric-status" [class.status-normal]="bloodPressure.systolic.status === 'normal'" [class.status-warning]="bloodPressure.systolic.status === 'warning'" [class.status-danger]="bloodPressure.systolic.status === 'danger'">{{ getStatusText(bloodPressure.systolic.status) }}</span>
+      <div class="metric-item" (click)="openMetricModal('hrv')">
+        <div class="metric-icon">🌿</div>
+        <div class="metric-value">{{ hrv.value }}<span class="metric-unit">{{ hrv.unit }}</span></div>
+        <div class="metric-label">HRV 变异性</div>
+        <span class="metric-status" [class.status-normal]="hrv.status === 'normal'" [class.status-warning]="hrv.status === 'warning'" [class.status-danger]="hrv.status === 'danger'">{{ getStatusText(hrv.status) }}</span>
       </div>
       <div class="metric-item" (click)="openMetricModal('bloodOxygen')">
         <div class="metric-icon">🫁</div>

+ 52 - 38
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.ts

@@ -2,6 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { Router, RouterModule } from '@angular/router';
 import { FormsModule } from '@angular/forms';
+import { HttpClient } from '@angular/common/http';
 import { ModalComponent } from '../../shared/components/modal/modal.component';
 import { PageHeaderComponent } from '../../shared/page-header/page-header.component';
 
@@ -85,24 +86,23 @@ export class MonitoringComponent implements OnInit, OnDestroy {
     trend: 'stable'
   };
 
-  // 血压数据
+  // HRV 数据(替代原血压,Apple Watch 测不了血压)
+  hrv: HealthMetric = {
+    value: 0,
+    unit: 'ms',
+    status: 'normal',
+    timestamp: new Date(),
+    trend: 'stable'
+  };
+
+  // 血压(保留字段以兼容旧弹窗/趋势代码,主界面不再展示)
   bloodPressure: { systolic: HealthMetric; diastolic: HealthMetric } = {
-    systolic: {
-      value: 120,
-      unit: 'mmHg',
-      status: 'normal',
-      timestamp: new Date(),
-      trend: 'stable'
-    },
-    diastolic: {
-      value: 80,
-      unit: 'mmHg',
-      status: 'normal',
-      timestamp: new Date(),
-      trend: 'stable'
-    }
+    systolic: { value: 0, unit: 'mmHg', status: 'normal', timestamp: new Date(), trend: 'stable' },
+    diastolic: { value: 0, unit: 'mmHg', status: 'normal', timestamp: new Date(), trend: 'stable' }
   };
 
+  recovery: { status: string; ratio: number; baseline: number; hrv: number; restingHr: number } | null = null;
+
   // 血氧数据
   bloodOxygen: HealthMetric = {
     value: 98,
@@ -121,22 +121,8 @@ export class MonitoringComponent implements OnInit, OnDestroy {
     symptoms: ['轻微腹痛', '疲劳']
   };
 
-  // 慢性病数据
-  chronicConditions: ChronicCondition[] = [
-    {
-      name: '轻度高血压',
-      severity: 'mild',
-      exerciseRestrictions: ['避免高强度运动', '运动前需热身', '避免突然发力'],
-      lastCheckup: new Date('2025-08-10'),
-      nextCheckupReminder: new Date('2025-11-10')
-    },
-    {
-      name: '颈椎不适',
-      severity: 'mild',
-      exerciseRestrictions: ['避免长时间低头', '加强颈部肌肉锻炼', '避免剧烈扭转'],
-      lastCheckup: new Date('2025-07-20')
-    }
-  ];
+  // 慢性病数据(当前无)
+  chronicConditions: ChronicCondition[] = [];
 
   // 是否为女性用户 (用于显示经期监测)
   isFemaleUser: boolean = true;
@@ -198,19 +184,47 @@ export class MonitoringComponent implements OnInit, OnDestroy {
   // 合并后的趋势图表
   trendChart: any = null;
 
-  constructor(private router: Router) {}
+  constructor(private router: Router, private http: HttpClient) {}
 
   ngOnInit(): void {
     this.generateMockHistoryData();
     this.initializeCyclePhases();
     this.calculateHealthScore();
-    // 延迟初始化图表,确保DOM和ECharts CDN已加载
-    setTimeout(() => {
-      this.initializeTrendChart();
-    }, 500);
-    
-    // 添加窗口调整大小事件监听器,使图表自适应
+    setTimeout(() => this.initializeTrendChart(), 500);
     window.addEventListener('resize', this.handleResize.bind(this));
+    this.loadRealMetrics();
+  }
+
+  private loadRealMetrics() {
+    this.http.get<any>('/api/health-metrics/latest').subscribe({
+      next: (res) => {
+        if (res.success && res.data) {
+          const d = res.data;
+          if (d.restingHeartRate) {
+            this.heartRate = { value: Number(d.restingHeartRate), unit: 'bpm', status: 'normal', timestamp: new Date(d.metricDate), trend: 'stable' };
+          }
+          if (d.hrvSdnn) {
+            this.hrv = { value: Number(d.hrvSdnn), unit: 'ms', status: 'normal', timestamp: new Date(d.metricDate), trend: 'stable' };
+          }
+          if (d.avgOxygenSaturation) {
+            this.bloodOxygen = { value: Number(d.avgOxygenSaturation), unit: '%', status: 'normal', timestamp: new Date(d.metricDate), trend: 'stable' };
+          }
+        }
+      }
+    });
+    this.http.get<any>('/api/health-metrics/recovery').subscribe({
+      next: (res) => {
+        if (res.success && res.data && res.data.enough) {
+          this.recovery = {
+            status: res.data.status,
+            ratio: res.data.ratio,
+            baseline: res.data.baseline30d,
+            hrv: res.data.today.hrv,
+            restingHr: res.data.today.restingHr,
+          };
+        }
+      }
+    });
   }
   
   ngOnDestroy(): void {