Conversation
…urve) ## Summary Port sleep science features from Thiagosian Health project: ### Backend (Express.js) - Repository, Service, Routes following SparkyFitness patterns (CommonJS, getClient, JSDoc Swagger) - 7 endpoints: sleep-debt, calculate-baseline, mctq-stats, daily-need, energy-curve, chronotype, data-sufficiency - PostgreSQL migration with 3 tables + 2 views + RLS policies ### Frontend (React + TypeScript) - 6 components: SleepDebtRing (SVG arc), EnergySchedule (Recharts), ChronotypeCard, SleepNeedBreakdown, SleepDebtHistory, SleepDebtBreakdown - API service with typed interfaces + TanStack Query hooks - Integrated into existing SleepReport page ### Algorithms (pure TypeScript libs) - Sleep Debt: exponential decay model (RISE/WHOOP) - Circadian Math: Two-Process Model (Borbely) - MCTQ: Munich Chronotype Questionnaire (Roenneberg) - Energy Curve: 24h curve with 96 points, zone detection, melatonin window ### i18n - 55 keys in all 16 supported languages - pt-BR translations sourced from original project ### Tests - Unit tests for sleep-debt, circadian-math, mctq-calculation (Vitest) ## Test Plan - `npm run lint` passes - `npx prettier . --check` passes - Unit tests: `npx vitest run src/tests/services/`
…mport, and extract backend constants
… fix chart scaling
Summary of ChangesHello @CodeWithCJ, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly expands the application's health tracking capabilities by integrating a sophisticated 'Sleep Science' feature. It provides users with in-depth insights into their sleep patterns, including metrics like sleep debt, chronotype, and a personalized energy curve, enhancing the overall user experience and data utility. The changes span both frontend and backend, ensuring a robust and interactive analytical tool. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive 'Sleep Science' feature, including backend services, database migrations, and frontend components for analyzing and displaying sleep debt, chronotype, and energy curves. The implementation is well-structured and based on established sleep science models. My review focuses on improving API consistency, database query performance, and code maintainability for this new feature set.
| sdFreeday: number | null; | ||
| socialJetlag: number | null; | ||
| } | null; | ||
| latestCalculation: Record<string, unknown> | null; |
There was a problem hiding this comment.
The type Record<string, unknown> for latestCalculation is not type-safe and hides the actual data structure. This can lead to runtime errors and makes the code harder to maintain.
It's better to define a specific interface that matches the shape of the data returned by the backend. Assuming the backend is updated to provide consistent camelCase properties (as suggested in a related comment), you could define and use an interface like this:
export interface SleepNeedCalculation {
id: string;
userId: string;
calculatedAt: string;
method: string;
calculatedNeed: number;
confidence: string;
basedOnDays: number;
sdWorkday?: number | null;
sdFreeday?: number | null;
sdWeek?: number | null;
socialJetlagHours?: number | null;
// ... and so on for other properties
}| latestCalculation: Record<string, unknown> | null; | |
| latestCalculation: SleepNeedCalculation | null; |
| /** | ||
| * Whoop Color System - Cores Oficiais (Máxima Precisão) | ||
| * | ||
| * Baseado nas especificações exatas do aplicativo Whoop: | ||
| * - Background Gradient: #283339 (topo) -> #101518 (base) | ||
| * - Recovery Status: #16EC06 (Verde), #FFDE00 (Amarelo), #FF0026 (Vermelho) | ||
| * - Métricas: #0093E7 (Strain), #7BA1BB (Sleep), #67AEE6 (Recovery) | ||
| * - CTA: #00F19F (Teal) | ||
| */ |
There was a problem hiding this comment.
The comments in this file are written in Portuguese, while the rest of the codebase, including file names and variable names, is in English. This inconsistency can make the code harder to understand and maintain for a broader team.
Please translate the comments to English to maintain consistency across the project. For example, "Cores Oficiais (Máxima Precisão)" could be "Official Colors (Maximum Precision)".
| const result = await client.query( | ||
| `SELECT | ||
| se.entry_date AS date, | ||
| se.duration_in_seconds, | ||
| se.bedtime, | ||
| se.wake_time, | ||
| COALESCE( | ||
| (SELECT SUM(ss.duration_in_seconds) FROM sleep_entry_stages ss WHERE ss.entry_id = se.id AND ss.stage_type = 'deep'), 0 | ||
| ) / 60.0 AS "deepSleepMinutes", | ||
| COALESCE( | ||
| (SELECT SUM(ss.duration_in_seconds) FROM sleep_entry_stages ss WHERE ss.entry_id = se.id AND ss.stage_type = 'rem'), 0 | ||
| ) / 60.0 AS "remSleepMinutes", | ||
| COALESCE( | ||
| (SELECT SUM(ss.duration_in_seconds) FROM sleep_entry_stages ss WHERE ss.entry_id = se.id AND ss.stage_type = 'light'), 0 | ||
| ) / 60.0 AS "lightSleepMinutes", | ||
| COALESCE( | ||
| (SELECT SUM(ss.duration_in_seconds) FROM sleep_entry_stages ss WHERE ss.entry_id = se.id AND ss.stage_type = 'awake'), 0 | ||
| ) / 60.0 AS "awakeMinutes", | ||
| se.duration_in_seconds / 3600.0 AS "sleepDurationHours", | ||
| EXTRACT(EPOCH FROM se.bedtime) * 1000 AS "sleepStartTimestampGMT", | ||
| EXTRACT(EPOCH FROM se.wake_time) * 1000 AS "sleepEndTimestampGMT", | ||
| se.sleep_score AS "sleepScore" | ||
| FROM sleep_entries se | ||
| WHERE se.user_id = $1 | ||
| AND se.entry_date >= CURRENT_DATE - INTERVAL '1 day' * $2 | ||
| AND se.duration_in_seconds > 0 | ||
| ORDER BY se.entry_date DESC`, | ||
| [userId, days] | ||
| ); | ||
| return result.rows; |
There was a problem hiding this comment.
The query in getSleepHistory uses four separate correlated subqueries to calculate the duration of each sleep stage. This can be inefficient as it may execute the subquery for each row returned from sleep_entries.
A more performant approach is to use a single LEFT JOIN with a subquery that uses conditional aggregation (FILTER clause). This calculates all stage durations in one pass over the sleep_entry_stages table.
To support this, also ensure a composite index exists on sleep_entry_stages (entry_id, stage_type).
const result = await client.query(
`SELECT
se.entry_date AS date,
se.duration_in_seconds,
se.bedtime,
se.wake_time,
COALESCE(stages.deep_sleep_minutes, 0) AS "deepSleepMinutes",
COALESCE(stages.rem_sleep_minutes, 0) AS "remSleepMinutes",
COALESCE(stages.light_sleep_minutes, 0) AS "lightSleepMinutes",
COALESCE(stages.awake_minutes, 0) AS "awakeMinutes",
se.duration_in_seconds / 3600.0 AS "sleepDurationHours",
EXTRACT(EPOCH FROM se.bedtime) * 1000 AS "sleepStartTimestampGMT",
EXTRACT(EPOCH FROM se.wake_time) * 1000 AS "sleepEndTimestampGMT",
se.sleep_score AS "sleepScore"
FROM sleep_entries se
LEFT JOIN (
SELECT
entry_id,
SUM(duration_in_seconds) FILTER (WHERE stage_type = 'deep') / 60.0 AS deep_sleep_minutes,
SUM(duration_in_seconds) FILTER (WHERE stage_type = 'rem') / 60.0 AS rem_sleep_minutes,
SUM(duration_in_seconds) FILTER (WHERE stage_type = 'light') / 60.0 AS light_sleep_minutes,
SUM(duration_in_seconds) FILTER (WHERE stage_type = 'awake') / 60.0 AS awake_minutes
FROM sleep_entry_stages
GROUP BY entry_id
) stages ON stages.entry_id = se.id
WHERE se.user_id = $1
AND se.entry_date >= CURRENT_DATE - INTERVAL '1 day' * $2
AND se.duration_in_seconds > 0
ORDER BY se.entry_date DESC`,
[userId, days]
);
return result.rows;| async function getMCTQStats(userId) { | ||
| log('info', `Getting MCTQ stats for user ${userId}`); | ||
|
|
||
| const profile = await sleepScienceRepository.getSleepProfile(userId); | ||
| const latestCalc = await sleepScienceRepository.getLatestCalculation(userId); | ||
| const dayClassifications = await sleepScienceRepository.getDayClassifications(userId); | ||
|
|
||
| return { | ||
| profile: profile | ||
| ? { | ||
| baselineSleepNeed: Number(profile.baseline_sleep_need) || DEFAULT_SLEEP_NEED_HOURS, | ||
| method: profile.sleep_need_method || 'default', | ||
| confidence: profile.sleep_need_confidence || 'low', | ||
| basedOnDays: profile.sleep_need_based_on_days || 0, | ||
| lastCalculated: profile.sleep_need_last_calculated, | ||
| sdWorkday: profile.sd_workday_hours ? Number(profile.sd_workday_hours) : null, | ||
| sdFreeday: profile.sd_freeday_hours ? Number(profile.sd_freeday_hours) : null, | ||
| socialJetlag: profile.social_jetlag_hours ? Number(profile.social_jetlag_hours) : null, | ||
| } | ||
| : null, | ||
| latestCalculation: latestCalc, | ||
| dayClassifications: dayClassifications.map((d) => ({ | ||
| dayOfWeek: d.day_of_week, | ||
| classifiedAs: d.classified_as, | ||
| meanWakeHour: d.mean_wake_hour ? Number(d.mean_wake_hour) : null, | ||
| varianceMinutes: d.variance_minutes ? Number(d.variance_minutes) : null, | ||
| sampleCount: d.sample_count, | ||
| })), | ||
| }; | ||
| } |
There was a problem hiding this comment.
The getMCTQStats function returns an object where the profile sub-object has its properties converted from snake_case to camelCase, but the latestCalculation sub-object is returned with its original snake_case properties from the database. This inconsistency makes it harder to define strong types on the frontend and can lead to bugs.
To ensure a consistent API response structure, please transform the latestCalc object to use camelCase properties, just as you've done for the profile object.
No description provided.