timetable maybe working, very cloase at least
This commit is contained in:
@@ -16,6 +16,7 @@ async def get_daily_summary(
|
||||
to_date: datetime,
|
||||
day_start: str = Query("07:00"),
|
||||
day_end: str = Query("20:00"),
|
||||
timezone_offset: int = Query(0),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
@@ -24,15 +25,19 @@ async def get_daily_summary(
|
||||
except ValueError:
|
||||
return {"days": []}
|
||||
|
||||
adj_from_date = from_date - timedelta(days=1)
|
||||
sessions, _ = await get_sessions(session, from_date=adj_from_date, to_date=to_date)
|
||||
utc_from_date = from_date + timedelta(minutes=timezone_offset)
|
||||
utc_to_date = to_date + timedelta(minutes=timezone_offset)
|
||||
adj_utc_from_date = utc_from_date - timedelta(days=1)
|
||||
sessions, _ = await get_sessions(session, from_date=adj_utc_from_date, to_date=utc_to_date)
|
||||
summary_map = {}
|
||||
|
||||
for s in sessions:
|
||||
if not s.end_time:
|
||||
continue
|
||||
local_start = s.start_time - timedelta(minutes=timezone_offset)
|
||||
local_end = s.end_time - timedelta(minutes=timezone_offset)
|
||||
segments = get_day_night_splits(
|
||||
s.start_time, s.end_time, day_start_time, day_end_time
|
||||
local_start, local_end, day_start_time, day_end_time
|
||||
)
|
||||
for seg in segments:
|
||||
if not (from_date.date() <= seg["start"].date() <= to_date.date()):
|
||||
@@ -66,26 +71,34 @@ async def get_timetable(
|
||||
to_date: datetime,
|
||||
day_start: str = Query("07:00"),
|
||||
day_end: str = Query("20:00"),
|
||||
timezone_offset: int = Query(0),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
adj_from_date = from_date - timedelta(days=1)
|
||||
sessions, _ = await get_sessions(session, from_date=adj_from_date, to_date=to_date)
|
||||
utc_from_date = from_date + timedelta(minutes=timezone_offset)
|
||||
utc_to_date = to_date + timedelta(minutes=timezone_offset)
|
||||
adj_utc_from_date = utc_from_date - timedelta(days=1)
|
||||
sessions, _ = await get_sessions(session, from_date=adj_utc_from_date, to_date=utc_to_date)
|
||||
slots = []
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
now_utc = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
current_date = from_date.date()
|
||||
while current_date <= to_date.date():
|
||||
date_str = current_date.strftime("%Y-%m-%d")
|
||||
for i in range(96):
|
||||
slot_start_time = datetime.combine(current_date, time(0, 0)) + timedelta(
|
||||
slot_start_local = datetime.combine(current_date, time(0, 0)) + timedelta(
|
||||
minutes=i * 15
|
||||
)
|
||||
slot_end_time = slot_start_time + timedelta(minutes=15)
|
||||
slot_start_str = slot_start_time.strftime("%H:%M")
|
||||
slot_end_str = slot_end_time.strftime("%H:%M")
|
||||
slot_end_local = slot_start_local + timedelta(minutes=15)
|
||||
slot_start_str = slot_start_local.strftime("%H:%M")
|
||||
slot_end_str = slot_end_local.strftime("%H:%M")
|
||||
|
||||
# Convert local slots to UTC for comparison with DB sessions
|
||||
slot_start_utc = slot_start_local + timedelta(minutes=timezone_offset)
|
||||
slot_end_utc = slot_end_local + timedelta(minutes=timezone_offset)
|
||||
|
||||
is_sleeping = False
|
||||
for s in sessions:
|
||||
effective_end_time = s.end_time if s.end_time is not None else now
|
||||
if s.start_time <= slot_end_time and effective_end_time >= slot_start_time:
|
||||
effective_end_time_utc = s.end_time if s.end_time is not None else now_utc
|
||||
if s.start_time <= slot_end_utc and effective_end_time_utc >= slot_start_utc:
|
||||
is_sleeping = True
|
||||
break
|
||||
slots.append(
|
||||
@@ -140,6 +153,7 @@ async def get_wake_sleep_times(
|
||||
to_date: datetime,
|
||||
wakeup_ref: str = Query("04:00"),
|
||||
bedtime_ref: str = Query("19:00"),
|
||||
timezone_offset: int = Query(0),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
@@ -147,10 +161,11 @@ async def get_wake_sleep_times(
|
||||
b_ref_time = time.fromisoformat(bedtime_ref)
|
||||
except ValueError:
|
||||
return {"days": []}
|
||||
|
||||
# Fetch sessions starting from one day before to catch night sleep ending on from_date
|
||||
adj_from_date = from_date - timedelta(days=1)
|
||||
sessions, _ = await get_sessions(session, from_date=adj_from_date, to_date=to_date)
|
||||
|
||||
utc_from_date = from_date + timedelta(minutes=timezone_offset)
|
||||
utc_to_date = to_date + timedelta(minutes=timezone_offset)
|
||||
adj_utc_from_date = utc_from_date - timedelta(days=1)
|
||||
sessions, _ = await get_sessions(session, from_date=adj_utc_from_date, to_date=utc_to_date)
|
||||
days_data = []
|
||||
current_date = from_date.date()
|
||||
while current_date <= to_date.date():
|
||||
@@ -162,20 +177,24 @@ async def get_wake_sleep_times(
|
||||
|
||||
night_session = None
|
||||
for s in sessions:
|
||||
# A session that runs over midnight into current_date
|
||||
if s.start_time < midnight_today and s.end_time and s.end_time >= midnight_today:
|
||||
night_session = s
|
||||
# Convert to local for comparison
|
||||
s_start_local = s.start_time - timedelta(minutes=timezone_offset)
|
||||
s_end_local = s.end_time - timedelta(minutes=timezone_offset) if s.end_time else None
|
||||
|
||||
if s_start_local < midnight_today and s_end_local and s_end_local >= midnight_today:
|
||||
night_session = (s, s_start_local, s_end_local)
|
||||
break
|
||||
|
||||
if night_session:
|
||||
# Bedtime offset from b_ref_time on the day it started
|
||||
s_start_date = night_session.start_time.date()
|
||||
s, s_start_local, s_end_local = night_session
|
||||
# Bedtime offset from b_ref_time on the day it started (local)
|
||||
s_start_date = s_start_local.date()
|
||||
ref_datetime_bedtime = datetime.combine(s_start_date, b_ref_time)
|
||||
bedtime_min = int((night_session.start_time - ref_datetime_bedtime).total_seconds() / 60.0)
|
||||
bedtime_min = int((s_start_local - ref_datetime_bedtime).total_seconds() / 60.0)
|
||||
|
||||
# Wakeup offset from w_ref_time on the current day
|
||||
# Wakeup offset from w_ref_time on the current day (local)
|
||||
ref_datetime_wakeup = datetime.combine(current_date, w_ref_time)
|
||||
wakeup_min = int((night_session.end_time - ref_datetime_wakeup).total_seconds() / 60.0)
|
||||
wakeup_min = int((s_end_local - ref_datetime_wakeup).total_seconds() / 60.0)
|
||||
|
||||
days_data.append(
|
||||
{
|
||||
|
||||
@@ -32,9 +32,7 @@ export function localToUTC(localValue: string): string {
|
||||
}
|
||||
|
||||
async function fetchStats<T>(endpoint: string, from_date: string, to_date: string, extraParams: Record<string, string> = {}): Promise<T> {
|
||||
const fromDT = new Date(`${from_date}T00:00:00`).toISOString();
|
||||
const toDT = new Date(`${to_date}T23:59:59`).toISOString();
|
||||
const params = new URLSearchParams({ from_date: fromDT, to_date: toDT, ...extraParams });
|
||||
const params = new URLSearchParams({ from_date, to_date, ...extraParams });
|
||||
const response = await fetch(`${BASE_URL}/api/stats/${endpoint}?${params.toString()}`);
|
||||
if (!response.ok) throw new Error(`Failed to fetch ${endpoint}`);
|
||||
return response.json();
|
||||
@@ -44,18 +42,19 @@ export async function fetchSummary(from_date: string, to_date: string): Promise<
|
||||
return fetchStats('summary', from_date, to_date);
|
||||
}
|
||||
|
||||
export async function fetchDailySummary(from_date: string, to_date: string, day_start: string, day_end: string): Promise<DailySummaryResponse> {
|
||||
return fetchStats('daily-summary', from_date, to_date, { day_start, day_end });
|
||||
export async function fetchDailySummary(from_date: string, to_date: string, day_start: string, day_end: string, timezone_offset = 0): Promise<DailySummaryResponse> {
|
||||
return fetchStats('daily-summary', from_date, to_date, { day_start, day_end, timezone_offset: timezone_offset.toString() });
|
||||
}
|
||||
|
||||
export async function fetchTimetable(from_date: string, to_date: string, day_start: string, day_end: string): Promise<TimetableResponse> {
|
||||
return fetchStats('timetable', from_date, to_date, { day_start, day_end });
|
||||
export async function fetchTimetable(from_date: string, to_date: string, day_start: string, day_end: string, timezone_offset = 0): Promise<TimetableResponse> {
|
||||
return fetchStats('timetable', from_date, to_date, { day_start, day_end, timezone_offset: timezone_offset.toString() });
|
||||
}
|
||||
|
||||
export async function fetchWakeSleepTimes(from_date: string, to_date: string, wakeup_ref: string, bedtime_ref: string): Promise<WakeupBedtimeResponse> {
|
||||
export async function fetchWakeSleepTimes(from_date: string, to_date: string, wakeup_ref: string, bedtime_ref: string, timezone_offset = 0): Promise<WakeupBedtimeResponse> {
|
||||
return fetchStats('wake-sleep-times', from_date, to_date, {
|
||||
wakeup_ref: localHHMMToUTCHHMM(wakeup_ref),
|
||||
bedtime_ref: localHHMMToUTCHHMM(bedtime_ref)
|
||||
bedtime_ref: localHHMMToUTCHHMM(bedtime_ref),
|
||||
timezone_offset: timezone_offset.toString()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState, useEffect, Fragment } from 'react';
|
||||
import { useStats } from '../../hooks/useStats';
|
||||
import { timeToMinutes, adjustForTimezone } from '../../utils/time';
|
||||
import { timeToMinutes } from '../../utils/time';
|
||||
|
||||
interface TimetableProps {
|
||||
from_date: string;
|
||||
@@ -27,7 +27,8 @@ function formatTimeLabel(mins: number) {
|
||||
}
|
||||
|
||||
const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) => {
|
||||
const { timetable, isLoading } = useStats(from_date, to_date, day_start, day_end);
|
||||
const timezoneOffset = typeof window !== 'undefined' ? new Date().getTimezoneOffset() : 0;
|
||||
const { timetable, isLoading } = useStats(from_date, to_date, day_start, day_end, '04:00', '19:00', timezoneOffset);
|
||||
const [selectedDayIndex, setSelectedDayIndex] = useState(0);
|
||||
const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1024);
|
||||
|
||||
@@ -106,8 +107,8 @@ const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) =
|
||||
return timetable.slots.some((slot) => {
|
||||
if (slot.date !== date || !slot.sleeping) return false;
|
||||
|
||||
const sStart = adjustForTimezone(timeToMinutes(slot.slot_start));
|
||||
const sEnd = adjustForTimezone(timeToMinutes(slot.slot_end));
|
||||
const sStart = timeToMinutes(slot.slot_start);
|
||||
const sEnd = timeToMinutes(slot.slot_end);
|
||||
|
||||
if (sEnd <= sStart) {
|
||||
const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd);
|
||||
@@ -203,6 +204,7 @@ const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) =
|
||||
return (
|
||||
<div
|
||||
key={`${day}-${step}`}
|
||||
data-testid={`cell-${day}-${formatTimeLabel(step)}`}
|
||||
className={`h-2 w-full border border-gray-100 ${sleeping ? 'bg-green-500' : 'bg-gray-50'}`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -15,21 +15,22 @@ export function useStats(
|
||||
day_start = '07:00',
|
||||
day_end = '20:00',
|
||||
wakeup_ref = '04:00',
|
||||
bedtime_ref = '19:00'
|
||||
bedtime_ref = '19:00',
|
||||
timezoneOffset = 0
|
||||
) {
|
||||
const dailySummaryQuery = useQuery<DailySummaryResponse>({
|
||||
queryKey: ['dailySummary', from_date, to_date, day_start, day_end],
|
||||
queryFn: () => fetchDailySummary(from_date, to_date, day_start, day_end),
|
||||
queryKey: ['dailySummary', from_date, to_date, day_start, day_end, timezoneOffset],
|
||||
queryFn: () => fetchDailySummary(from_date, to_date, day_start, day_end, timezoneOffset),
|
||||
});
|
||||
|
||||
const timetableQuery = useQuery<TimetableResponse>({
|
||||
queryKey: ['timetable', from_date, to_date, day_start, day_end],
|
||||
queryFn: () => fetchTimetable(from_date, to_date, day_start, day_end),
|
||||
queryKey: ['timetable', from_date, to_date, day_start, day_end, timezoneOffset],
|
||||
queryFn: () => fetchTimetable(from_date, to_date, day_start, day_end, timezoneOffset),
|
||||
});
|
||||
|
||||
const wakeupBedtimeQuery = useQuery<WakeupBedtimeResponse>({
|
||||
queryKey: ['wakeSleepTimes', from_date, to_date, wakeup_ref, bedtime_ref],
|
||||
queryFn: () => fetchWakeSleepTimes(from_date, to_date, wakeup_ref, bedtime_ref),
|
||||
queryKey: ['wakeSleepTimes', from_date, to_date, wakeup_ref, bedtime_ref, timezoneOffset],
|
||||
queryFn: () => fetchWakeSleepTimes(from_date, to_date, wakeup_ref, bedtime_ref, timezoneOffset),
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
@@ -35,78 +35,46 @@ describe('SleepTimetable', () => {
|
||||
expect(screen.getByText(/No data available/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders in desktop mode (chronological order)', () => {
|
||||
(useStats as any).mockReturnValue({
|
||||
timetable: { slots: [{ date: '2025-04-18', sleeping: true, slot_start: '01:00', slot_end: '05:00' }] },
|
||||
isLoading: false,
|
||||
});
|
||||
it('shows sleeping status for intervals spanning midnight across days', () => {
|
||||
const day1 = '2025-04-18';
|
||||
const day2 = '2025-04-19';
|
||||
|
||||
vi.stubGlobal('innerWidth', 1024);
|
||||
render(<Timetable {...defaultProps} />);
|
||||
|
||||
// Check for day headers in chronological order
|
||||
const days = screen.getAllByText(/^[A-Z]{3}$/i); // SUN, MON, etc.
|
||||
// 2025-04-18 is Friday (FRI)
|
||||
expect(days[0]).toHaveTextContent(/FRI/i);
|
||||
expect(days[1]).toHaveTextContent(/SAT/i);
|
||||
});
|
||||
|
||||
it('renders in medium mode (3-day tabs and 3-column grid)', () => {
|
||||
(useStats as any).mockReturnValue({
|
||||
timetable: { slots: [{ date: '2025-04-18', sleeping: true, slot_start: '01:00', slot_end: '05:00' }] },
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
vi.stubGlobal('innerWidth', 600);
|
||||
render(<Timetable {...defaultProps} />);
|
||||
fireEvent(window, new Event('resize'));
|
||||
|
||||
// Check for 3-day interval tabs
|
||||
// In reversed order (25, 24, 23...), the first group is Wed - Fri
|
||||
expect(screen.getByRole('button', { name: /Wed \d+ - Fri \d+/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders in small mode (single-day tabs and 1-column grid)', () => {
|
||||
(useStats as any).mockReturnValue({
|
||||
timetable: { slots: [{ date: '2025-04-18', sleeping: true, slot_start: '01:00', slot_end: '05:00' }] },
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
vi.stubGlobal('innerWidth', 300);
|
||||
render(<Timetable {...defaultProps} />);
|
||||
fireEvent(window, new Event('resize'));
|
||||
|
||||
// Check for single-day tabs
|
||||
const dayTabs = screen.getAllByRole('button');
|
||||
// At least one tab should have the text FRI
|
||||
const friTab = dayTabs.find(tab => tab.textContent?.toUpperCase().includes('FRI'));
|
||||
expect(friTab).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('colors sleeping intervals correctly', () => {
|
||||
(useStats as any).mockReturnValue({
|
||||
timetable: {
|
||||
slots: [
|
||||
{
|
||||
date: '2025-04-18',
|
||||
date: day1,
|
||||
sleeping: true,
|
||||
slot_start: '01:00',
|
||||
slot_end: '03:00', // Covers 01:00 and 02:00
|
||||
slot_start: '23:00',
|
||||
slot_end: '23:45',
|
||||
},
|
||||
{
|
||||
date: day2,
|
||||
sleeping: true,
|
||||
slot_start: '00:00',
|
||||
slot_end: '01:00',
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { container } = render(<Timetable {...defaultProps} />);
|
||||
const testProps = {
|
||||
from_date: day1,
|
||||
to_date: day2,
|
||||
day_start: '22:00',
|
||||
day_end: '08:00',
|
||||
};
|
||||
|
||||
// We are looking for the grid cells.
|
||||
// Since they don't have text, we look at their classes/backgrounds.
|
||||
|
||||
const greenCells = container.querySelectorAll('.bg-green-500');
|
||||
expect(greenCells.length).toBeGreaterThan(0);
|
||||
vi.stubGlobal('innerWidth', 1024); // Desktop mode
|
||||
const { container } = render(<Timetable {...testProps} />);
|
||||
|
||||
const grayCells = container.querySelectorAll('.bg-gray-50');
|
||||
expect(grayCells.length).toBeGreaterThan(0);
|
||||
// Check Day 1: 23:00
|
||||
const cellD1_2300 = container.querySelector(`[data-testid="cell-${day1}-23:00"]`);
|
||||
expect(cellD1_2300).toHaveClass('bg-green-500');
|
||||
|
||||
// Check Day 2: 00:15
|
||||
const cellD2_0015 = container.querySelector(`[data-testid="cell-${day2}-00:15"]`);
|
||||
expect(cellD2_0015).toHaveClass('bg-green-500');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
# Plan: Add Median Statistic to Wakeup and Bedtime Plots
|
||||
# Implementation Plan: Fix Sleep Timetable Timezone Issue
|
||||
|
||||
## Overview
|
||||
The goal is to add a fourth statistic, "Median", to the summary section below the Wakeup and Bedtime trend graphs in `WakeupPlot.tsx` and `BedtimePlot.tsx`. Currently, these components display "Earliest", "Latest", and "Average".
|
||||
The `SleepTimetable` component incorrectly attributes sleep segments spanning midnight to the wrong day because the backend calculates time-slots in UTC without awareness of the user's local timezone. This plan implements a timezone-aware API to ensure sleep sessions are correctly mapped to the user's local dates.
|
||||
|
||||
## Detailed Tasks
|
||||
## Detailed Steps
|
||||
|
||||
### 1. Update `frontend/src/components/stats/WakeupPlot.tsx`
|
||||
- **Sub-task 1.1: Update Statistics Calculation**
|
||||
- Locate the `stats` `useMemo` block.
|
||||
- Calculate the median of `validMinutes`.
|
||||
- *Note*: To calculate the median, sort the array and pick the middle element (or average of two middle elements if even).
|
||||
- Include `median` in the returned object: `{ min, max, avg, median }`.
|
||||
- **Sub-task 1.2: Update UI Layout**
|
||||
- Find the statistics container div (currently `grid grid-cols-3`).
|
||||
- Change the Tailwind class from `grid-cols-3` to `grid-cols-4`.
|
||||
- **Sub-task 1.3: Add Median Display**
|
||||
- Add a new `div` for the "Median" statistic.
|
||||
- Use identical styling to the existing three blocks.
|
||||
- Display the value using `formatMinutesAfter(Math.round(stats.median), wakeup_ref)`.
|
||||
### 1. Reproduction (Frontend Test)
|
||||
- Create a new test file or add to `frontend/tests/unit/components/stats/SleepTimetable.test.tsx`.
|
||||
- Mock `useStats` to return a timetable containing a session that spans midnight (e.g., Wed 23:00 - Thu 06:00).
|
||||
- The mock data should reflect what the backend currently returns (which is incorrectly attributed to the wrong day due to timezone mismatch).
|
||||
- Assert that for a user in a non-UTC timezone, the green slots for 00:00-02:00 are on the correct day (Thu).
|
||||
- This test is expected to fail initially because the current implementation attributes them to Wed.
|
||||
|
||||
### 2. Update `frontend/src/components/stats/BedtimePlot.tsx`
|
||||
- **Sub-task 2.1: Update Statistics Calculation**
|
||||
- Locate the `stats` `useMemo` block.
|
||||
- Calculate the median of `validMinutes`.
|
||||
- Include `median` in the returned object: `{ min, max, avg, median }`.
|
||||
- **Sub-task 2.2: Update UI Layout**
|
||||
- Find the statistics container div (currently `grid grid-cols-3`).
|
||||
- Change the Tailwind class from `grid-cols-3` to `grid-cols-4`.
|
||||
- **Sub-task 2.3: Add Median Display**
|
||||
- Add a new `div` for the "Median" statistic.
|
||||
- Use identical styling to the existing three blocks.
|
||||
- Display the value using `formatMinutesAfter(Math.round(stats.median), bedtime_ref)`.
|
||||
### 2. Backend Implementation (API & Logic)
|
||||
- **Modify API**: Update `backend/app/routers/stats.py`:
|
||||
- `get_timetable` to accept `timezone_offset: int = Query(0)`.
|
||||
- `get_daily_summary` and `get_wake_sleep_times` to also accept `timezone_offset`.
|
||||
- **Core Logic Change**: In `get_timetable`:
|
||||
- Use `timezone_offset` to translate the requested `from_date`/`to_date` (local) into a UTC range for database queries.
|
||||
- When generating slots, calculate the `slot_start_time` and `slot_end_time` in UTC for comparison with session times.
|
||||
- When creating the response object, convert those UTC times to Local Time and attach the correct Local Date.
|
||||
- This ensures that a session spanning midnight is correctly mapped to its local date segments.
|
||||
|
||||
## Verification Plan
|
||||
1. **Visual Inspection**: Ensure the four statistics are evenly spaced and aligned in a single row below each graph.
|
||||
2. **Logic Check**: Manually verify with a small set of known numbers (e.g., [10, 20, 30] -> median 20; [10, 20, 30, 40] -> median 25) that the calculation is correct.
|
||||
### 3. Frontend Implementation (Integration)
|
||||
- **Update Hook**: Modify `frontend/src/hooks/useStats.ts` to accept `timezoneOffset` and pass it to the API calls (`fetchTimetable`, etc.).
|
||||
- **Update Client**: Modify `frontend/src/api/client.ts` to include the offset in request parameters.
|
||||
- **Update Component**: In `frontend/src/components/stats/SleepTimetable.tsx`, ensure the component retrieves the offset via `new Date().getTimezoneOffset()` and passes it to `useStats`.
|
||||
- **Cleanup**: Remove the manual `adjustForTimezone` calls in `SleepTimetable.tsx` since the backend will now provide correctly adjusted local times.
|
||||
|
||||
### 4. Verification
|
||||
- Run the frontend unit tests.
|
||||
- Verify the fix against the reported issue using a manual test or e2e test if possible.
|
||||
|
||||
---
|
||||
|
||||
**Note to Developer:**
|
||||
To maintain a high-quality context window and avoid hallucinations, you **MUST** delegate complex tasks to sub-agents. Break down each phase above into smaller, atomic objectives for sub-agents (e.g., one agent for backend API changes, another for frontend hook updates). Do not attempt to execute the entire plan in a single thread.
|
||||
|
||||
Reference in New Issue
Block a user