2 Commits

Author SHA1 Message Date
jacobh2 8852cf202e Fix timezones
Build Backend / build (push) Successful in 1m54s
Build Frontend / build (push) Successful in 3m43s
2026-05-30 16:55:09 +02:00
jacobh2 91184cfc96 Partly fixing timetable 2026-05-30 16:55:09 +02:00
7 changed files with 35 additions and 32 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta, timezone
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
@@ -71,6 +71,7 @@ async def get_timetable(
adj_from_date = from_date - timedelta(days=1) adj_from_date = from_date - timedelta(days=1)
sessions, _ = await get_sessions(session, from_date=adj_from_date, to_date=to_date) sessions, _ = await get_sessions(session, from_date=adj_from_date, to_date=to_date)
slots = [] slots = []
now = datetime.now(timezone.utc).replace(tzinfo=None)
current_date = from_date.date() current_date = from_date.date()
while current_date <= to_date.date(): while current_date <= to_date.date():
date_str = current_date.strftime("%Y-%m-%d") date_str = current_date.strftime("%Y-%m-%d")
@@ -83,9 +84,8 @@ async def get_timetable(
slot_end_str = slot_end_time.strftime("%H:%M") slot_end_str = slot_end_time.strftime("%H:%M")
is_sleeping = False is_sleeping = False
for s in sessions: for s in sessions:
if s.start_time <= slot_end_time and ( effective_end_time = s.end_time if s.end_time is not None else now
s.end_time is None or s.end_time >= slot_start_time if s.start_time <= slot_end_time and effective_end_time >= slot_start_time:
):
is_sleeping = True is_sleeping = True
break break
slots.append( slots.append(
@@ -1,6 +1,6 @@
import { useMemo, useState, useEffect, Fragment } from 'react'; import { useMemo, useState, useEffect, Fragment } from 'react';
import { useStats } from '../../hooks/useStats'; import { useStats } from '../../hooks/useStats';
import { timeToMinutes } from '../../utils/time'; import { timeToMinutes, adjustForTimezone } from '../../utils/time';
interface TimetableProps { interface TimetableProps {
from_date: string; from_date: string;
@@ -106,8 +106,8 @@ const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) =
return timetable.slots.some((slot) => { return timetable.slots.some((slot) => {
if (slot.date !== date || !slot.sleeping) return false; if (slot.date !== date || !slot.sleeping) return false;
const sStart = timeToMinutes(slot.slot_start); const sStart = adjustForTimezone(timeToMinutes(slot.slot_start));
const sEnd = timeToMinutes(slot.slot_end); const sEnd = adjustForTimezone(timeToMinutes(slot.slot_end));
if (sEnd <= sStart) { if (sEnd <= sStart) {
const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd); const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd);
+5 -1
View File
@@ -68,7 +68,11 @@ export function formatElapsedTime(utcTimestamp: string): string {
* @returns formatted datetime string * @returns formatted datetime string
*/ */
export function formatDateTime(date: Date | string): string { export function formatDateTime(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date; let dateToParse = date;
if (typeof date === 'string' && !date.endsWith('Z') && !date.includes('+')) {
dateToParse = date + 'Z';
}
const d = typeof dateToParse === 'string' ? new Date(dateToParse) : dateToParse;
return d.toLocaleString('sv-SE', { return d.toLocaleString('sv-SE', {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
@@ -62,8 +62,8 @@ describe('SleepTimetable', () => {
fireEvent(window, new Event('resize')); fireEvent(window, new Event('resize'));
// Check for 3-day interval tabs // Check for 3-day interval tabs
// In reversed order (25, 24, 23...), the first group is Fri - Wed // In reversed order (25, 24, 23...), the first group is Wed - Fri
expect(screen.getByRole('button', { name: /Fri \d+ - Wed \d+/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Wed \d+ - Fri \d+/i })).toBeInTheDocument();
}); });
it('renders in small mode (single-day tabs and 1-column grid)', () => { it('renders in small mode (single-day tabs and 1-column grid)', () => {
+16
View File
@@ -11,6 +11,7 @@ import {
timeToMinutes, timeToMinutes,
adjustForTimezone, adjustForTimezone,
getLocalOffsetFromReference, getLocalOffsetFromReference,
formatDateTime,
} from '../../src/utils/time'; } from '../../src/utils/time';
describe('formatMinutesAfter', () => { describe('formatMinutesAfter', () => {
@@ -125,6 +126,21 @@ describe('adjustForTimezone', () => {
}); });
}); });
describe('formatDateTime', () => {
it('treats string without Z as UTC', () => {
const dateStr = '2024-01-15T10:00:00';
const dateWithZ = '2024-01-15T10:00:00Z';
expect(formatDateTime(dateStr)).toBe(formatDateTime(dateWithZ));
});
it('formats a Date object correctly', () => {
const date = new Date('2024-01-15T10:00:00Z');
// The output depends on the local timezone, so we check for part of it
const result = formatDateTime(date);
expect(result).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/);
});
});
describe('getLocalOffsetFromReference', () => { describe('getLocalOffsetFromReference', () => {
it('returns local offset from reference in minutes', () => { it('returns local offset from reference in minutes', () => {
const utcMins = 600; // 10:00 UTC const utcMins = 600; // 10:00 UTC
-22
View File
@@ -1,22 +0,0 @@
# Sleep Timetable Bug Fixes
## Identified Issues
1. **Pre-selection**: Component doesn't always pre-select the latest date across all tiers.
2. **Grid Layout (Medium Tier)**: Grid columns are hardcoded to 3, causing breakage when fewer than 3 days are displayed at the end of a range.
3. **Selector Information**: Tabs only show weekday names, making it hard to distinguish dates.
## Implementation Plan
### 1. Fix Pre-selection
- Add a `useEffect` to set `selectedDayIndex` to the last index of the `days` array whenever the `days` list is initialized or updated. This ensures "today" (the latest date) is selected by default.
### 2. Fix Grid Alignment
- Refactor the `gridTemplateColumns` logic for both the header and time grid.
- Instead of hardcoding tiers (e.g., `repeat(3, 1fr)`), use the actual number of days in the current view: `4rem repeat(${visibleDays.length}, 1fr)`. This allows the grid to gracefully handle 1, 2, or 3 columns.
### 3. Enhance Selector Labels
- Implement a `formatDayLabel` helper that combines the short weekday name and the day of the month (e.g., "FRI 24").
- Update `tabGroups` generation for both 'medium' (range) and 'small' (single day) tiers to use this descriptive label.
### 4. UI Compactness
- Adjust spacing (margins/paddings) in the mobile/medium view to reduce overall component height and keep selectors closer to the grid.
+5
View File
@@ -0,0 +1,5 @@
# Things I need to tell my AI to do
- [ ] Check so that all tests actually make sense, maybe some are now obsolete, and some are missing
- [ ] Add a test coverage metric to both frontend and backend so that we can check how much of the code is tested. I would like >=90%
- [ ] Go over Claude's slop-cleanup-stuff