Drying out
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
# Implementation Plan: Fix Tests and Playground Components
|
||||
|
||||
## Goal
|
||||
1. Fix the malformed `SleepTimetable.test.tsx`.
|
||||
2. Resolve console errors in `WakeupPlot` and `BedtimePlot` playground components.
|
||||
3. Ensure 100% test pass rate using TDD.
|
||||
|
||||
## Phase 1: Fix Malformed Test File
|
||||
- **Action:** Clean up `frontend/tests/unit/components/stats/SleepTimetable.test.tsx` by removing duplicated and malformed test blocks.
|
||||
- **Verification:** Run `npm run test` to ensure the parse error is resolved and existing tests pass.
|
||||
|
||||
## Phase 2: Fix Playground Components (`WakeupPlot`, `BedtimePlot`)
|
||||
- **Investigation:**
|
||||
- Examine `frontend/src/utils/time.js` to find the `split` error in `timeToMinutes`.
|
||||
- Trace calls from `frontend/src/components/stats/WakeupPlot.js` and `frontend/src/components/stats/BedtimePlot.js`.
|
||||
- **Resolution:** Fix the `undefined` value being passed to time utilities, ensuring components load in the playground without console errors.
|
||||
- **Verification:** Validate via component tests and ensure no regressions.
|
||||
|
||||
## Phase 3: Final TDD Verification
|
||||
- **Mandatory Loop:**
|
||||
1. Run `npm run test`.
|
||||
2. Run `npm run test:all`.
|
||||
- **Requirement:** Iterate until all tests pass flawlessly (100% success) and the component fixes are verified. No task is complete until both commands return zero errors.
|
||||
@@ -36,7 +36,7 @@ async def read_active_session(session: AsyncSession = Depends(get_session)):
|
||||
from app.crud import get_active_session
|
||||
|
||||
active = await get_active_session(session)
|
||||
return {"session": active}
|
||||
return await get_active_session(session)
|
||||
|
||||
|
||||
@router.get("", response_model=SessionListResponse)
|
||||
@@ -87,7 +87,7 @@ async def delete_session_endpoint(
|
||||
return None
|
||||
|
||||
|
||||
@router.put("/{session_id}", response_model=None)
|
||||
@router.patch("/{session_id}", response_model=SleepSessionRead)
|
||||
async def update_session_endpoint(
|
||||
session_id: int,
|
||||
payload: SleepSessionUpdate,
|
||||
|
||||
@@ -2,26 +2,7 @@ import React, { useState } from 'react';
|
||||
import { useSessions } from '../../hooks/useSleepSession';
|
||||
import { Edit2, Check, X } from 'lucide-react';
|
||||
import type { SleepSessionRead } from '../../types';
|
||||
|
||||
function formatDateTime(isoString: string): string {
|
||||
const iso = isoString.endsWith('Z') ? isoString : isoString + 'Z';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString('sv-SE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}).replace(',', ' ');
|
||||
}
|
||||
|
||||
function toDatetimeLocal(isoString: string): string {
|
||||
const iso = isoString.endsWith('Z') ? isoString : isoString + 'Z';
|
||||
const d = new Date(iso);
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
import { formatDateTime, toLocalDatetimeInputValue, getDurationMinutes } from '../../utils/time';
|
||||
|
||||
export function SessionList() {
|
||||
const { sessions, isLoading, updateSession } = useSessions();
|
||||
@@ -50,8 +31,8 @@ export function SessionList() {
|
||||
|
||||
const startEditing = (s: SleepSessionRead) => {
|
||||
setEditingId(s.id);
|
||||
setEditStart(toDatetimeLocal(s.start_time));
|
||||
setEditEnd(s.end_time ? toDatetimeLocal(s.end_time) : '');
|
||||
setEditStart(toLocalDatetimeInputValue(new Date(s.start_time)));
|
||||
setEditEnd(s.end_time ? toLocalDatetimeInputValue(new Date(s.end_time)) : '');
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -120,10 +101,11 @@ export function SessionList() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-blue-600 font-mono text-sm">
|
||||
{s.end_time && s.start_time
|
||||
? `${Math.round((new Date(s.end_time + 'Z').getTime() - new Date(s.start_time + 'Z').getTime()) / 60000)} min`
|
||||
: '—'}
|
||||
</span>
|
||||
{s.end_time && s.start_time
|
||||
? `${getDurationMinutes(s.start_time, s.end_time)} min`
|
||||
: '—'}
|
||||
</span>
|
||||
|
||||
<Edit2 size={14} className="text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useStats } from '../../hooks/useStats';
|
||||
import { formatMinutesAfter, getToday, getLocalOffsetFromReference, timeToMinutes } from '../../utils/time';
|
||||
import { formatMinutesAfter, getToday, getLocalOffsetFromReference } from '../../utils/time';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
@@ -35,16 +35,7 @@ const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_r
|
||||
|
||||
let offsetFromRef = 0;
|
||||
if (day.wakeup_minutes !== null) {
|
||||
const refMins = timeToMinutes(wakeup_ref);
|
||||
// If wakeup_minutes is significantly smaller than the reference,
|
||||
// it's likely already an offset (relative), not absolute minutes from midnight.
|
||||
const isLikelyOffset = day.wakeup_minutes < refMins && day.wakeup_minutes < 120;
|
||||
|
||||
if (isLikelyOffset) {
|
||||
offsetFromRef = day.wakeup_minutes;
|
||||
} else {
|
||||
offsetFromRef = day.wakeup_minutes - refMins;
|
||||
}
|
||||
offsetFromRef = getLocalOffsetFromReference(day.wakeup_minutes, wakeup_ref);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { toLocalDatetimeInputValue, toUTCFromLocalInput } from '../../utils/time';
|
||||
|
||||
interface TimeAdjusterProps {
|
||||
onTimeChange: (time: string) => void;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
function toLocalDatetimeInputValue(date: Date): string {
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
function toUTCFromLocalInput(inputValue: string): string {
|
||||
const [datePart, timePart] = inputValue.split('T');
|
||||
const [year, month, day] = datePart.split('-').map(Number);
|
||||
const [hours, minutes] = timePart.split(':').map(Number);
|
||||
const localDate = new Date(year, month - 1, day, hours, minutes);
|
||||
return localDate.toISOString();
|
||||
}
|
||||
|
||||
const TimeAdjuster: React.FC<TimeAdjusterProps> = ({ onTimeChange, defaultValue }) => {
|
||||
const [value, setValue] = useState(() => {
|
||||
if (defaultValue) {
|
||||
|
||||
@@ -62,6 +62,45 @@ export function formatElapsedTime(utcTimestamp: string): string {
|
||||
return `${mins}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Date or ISO string to a human-readable datetime string (YYYY-MM-DD HH:mm)
|
||||
* @param date - Date object or ISO string
|
||||
* @returns formatted datetime string
|
||||
*/
|
||||
export function formatDateTime(date: Date | string): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.toLocaleString('sv-SE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}).replace(',', ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a datetime-local input value to an ISO UTC string
|
||||
* @param inputValue - datetime-local string (YYYY-MM-DDTHH:mm)
|
||||
* @returns ISO UTC string
|
||||
*/
|
||||
export function toUTCFromLocalInput(inputValue: string): string {
|
||||
const d = new Date(inputValue);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate duration in minutes between two dates or ISO strings
|
||||
* @param start - Start date object or ISO string
|
||||
* @param end - End date object or ISO string
|
||||
* @returns duration in minutes
|
||||
*/
|
||||
export function getDurationMinutes(start: Date | string, end: Date | string): number {
|
||||
const startDate = typeof start === 'string' ? new Date(start) : start;
|
||||
const endDate = typeof end === 'string' ? new Date(end) : end;
|
||||
return Math.round((endDate.getTime() - startDate.getTime()) / 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a date to day boundaries (start or end of day)
|
||||
* @param date - Date to clamp
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
clampToDay,
|
||||
parseTime,
|
||||
formatMinutes,
|
||||
getDaysAgo,
|
||||
getToday,
|
||||
getDaysAgo,
|
||||
timeToMinutes,
|
||||
adjustForTimezone,
|
||||
getLocalOffsetFromReference,
|
||||
} from '../../src/utils/time';
|
||||
|
||||
describe('formatMinutesAfter', () => {
|
||||
@@ -113,3 +115,24 @@ describe('timeToMinutes', () => {
|
||||
expect(timeToMinutes('23:59')).toBe(1439);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adjustForTimezone', () => {
|
||||
it('adjusts minutes by the local timezone offset', () => {
|
||||
const utcMins = 600; // 10:00 UTC
|
||||
const adjusted = adjustForTimezone(utcMins);
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
expect(adjusted).toBe(utcMins - offset);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocalOffsetFromReference', () => {
|
||||
it('returns local offset from reference in minutes', () => {
|
||||
const utcMins = 600; // 10:00 UTC
|
||||
const refTime = '07:00'; // 07:00 reference
|
||||
const offset = getLocalOffsetFromReference(utcMins, refTime);
|
||||
|
||||
const localMins = adjustForTimezone(utcMins);
|
||||
const refMins = timeToMinutes(refTime);
|
||||
expect(offset).toBe(localMins - refMins);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
# Investigation Plan: Bedtime Graph Issue
|
||||
# Plan: Fix Timezone issues in WakeupPlot
|
||||
|
||||
## Objective
|
||||
Investigate why the "Bedtime" graph shows unexpected times when a specific `bedtime_ref` is set.
|
||||
## Problem
|
||||
`WakeupPlot.tsx` uses an incorrect manual calculation for timezone offsets and a dubious `isLikelyOffset` heuristic. This causes incorrect time displays (e.g., subtracting the offset instead of adding it).
|
||||
|
||||
## Current Understanding
|
||||
- The `BedtimePlot.tsx` component displays a graph where the Y-axis (or values) are formatted using `formatMinutesAfter(value, bedtime_ref)`.
|
||||
- The data comes from the `/api/stats/wake-sleep-times` endpoint.
|
||||
- The backend calculates `bedtime_minutes` based on sessions that started on or before the `bedtime_ref`.
|
||||
## Goal
|
||||
Align `WakeupPlot.tsx` with `BedtimePlot.tsx` by using the existing `getLocalOffsetFromReference` utility to correctly apply client-side timezone adjustments.
|
||||
|
||||
## Investigation Steps
|
||||
## Steps
|
||||
1. **Modify `frontend/src/components/stats/WakeupPlot.tsx`**:
|
||||
- Remove the `isLikelyOffset` logic and manual offset calculations.
|
||||
- Use `getLocalOffsetFromReference(day.wakeup_minutes, wakeup_ref)` to calculate the correct local offset.
|
||||
2. **Verify**:
|
||||
- Ensure the component renders correctly with the new logic.
|
||||
|
||||
### Step 1: Analyze Backend Logic
|
||||
- [ ] Examine `get_wake_sleep_times` in `backend/app/routers/stats.py` to understand how `bedtime_minutes` is calculated.
|
||||
- [ ] Verify if the logic correctly identifies "bedtime" for various sleep session scenarios (e.g., sessions crossing midnight).
|
||||
- [ ] Identify potential edge cases where the `s.start_time.time() <= b_ref_time` condition might be problematic.
|
||||
|
||||
### Step 2: Analyze Frontend Logic
|
||||
- [ ] Examine `BedtimePlot.tsx` and `useStats.ts` to see how the data is consumed and formatted.
|
||||
- [ ] Understand how `formatMinutesAfter` interacts with the values returned by the backend.
|
||||
|
||||
### Step 3: Data Simulation & Verification
|
||||
- [ ] Create a set of test cases (mocked data) representing different sleep scenarios.
|
||||
- [ ] Manually calculate the expected `bedtime_minutes` for these scenarios using the current logic.
|
||||
- [ ] Compare with what the user reported to confirm the bug's nature.
|
||||
|
||||
### Step 4: Propose and Implement Fixes
|
||||
- [ ] Refine the backend logic to better represent "bedtime" if current logic is insufficient.
|
||||
- [ ] Ensure the frontend correctly displays the intended time.
|
||||
- [ ] Verify that changes do not break other parts of the stats page (e.g., Wakeup times).
|
||||
|
||||
## Future Tasks
|
||||
- [ ] Run tests to ensure correctness after implementation.
|
||||
- [ ] Finalize documentation/explanation of the fix.
|
||||
## Files to change
|
||||
- `frontend/src/components/stats/WakeupPlot.tsx`
|
||||
|
||||
Reference in New Issue
Block a user