Soon fixing timeone in wakeup plot
This commit is contained in:
@@ -2,6 +2,22 @@ import type { SleepSessionCreate, SleepSessionUpdate, SleepSessionRead, SessionL
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
/**
|
||||
* Convert a local HH:MM time string to its UTC equivalent.
|
||||
* @param hhmm - Local time as "HH:MM"
|
||||
* @returns UTC time as "HH:MM"
|
||||
*/
|
||||
export function localHHMMToUTCHHMM(hhmm: string): string {
|
||||
const [hours, minutes] = hhmm.split(":").map(Number);
|
||||
const offsetInMinutes = new Date().getTimezoneOffset();
|
||||
// UTC = Local - offsetInMinutes (where offset is negative for East of UTC)
|
||||
const totalMinutesUTC = (hours * 60 + minutes) + offsetInMinutes;
|
||||
const normalizedMinutes = ((totalMinutesUTC % 1440) + 1440) % 1440;
|
||||
const h = Math.floor(normalizedMinutes / 60);
|
||||
const m = normalizedMinutes % 60;
|
||||
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function localToUTC(localValue: string): string {
|
||||
return new Date(localValue).toISOString();
|
||||
}
|
||||
@@ -24,7 +40,89 @@ export async function fetchTimetable(from_date: string, to_date: string, day_sta
|
||||
}
|
||||
|
||||
export async function fetchWakeSleepTimes(from_date: string, to_date: string, wakeup_ref: string, bedtime_ref: string): Promise<any> {
|
||||
return fetchStats('wake-sleep-times', from_date, to_date, { wakeup_ref, bedtime_ref });
|
||||
return fetchStats('wake-sleep-times', from_date, to_date, {
|
||||
wakeup_ref: localHHMMToUTCHHMM(wakeup_ref),
|
||||
bedtime_ref: localHHMMToUTCHHMM(bedtime_ref)
|
||||
});
|
||||
}
|
||||
|
||||
// Session API functions
|
||||
export async function fetchActiveSession(): Promise<SleepSessionRead | null> {
|
||||
const response = await fetch(`${BASE_URL}/api/sessions/active`);
|
||||
if (!response.ok) return null;
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function fetchSessions(): Promise<SessionListResponse> {
|
||||
const response = await fetch(`${BASE_URL}/api/sessions`);
|
||||
if (!response.ok) throw new Error('Failed to fetch sessions');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function createSession(payload: SleepSessionCreate): Promise<SleepSessionRead> {
|
||||
const response = await fetch(`${BASE_URL}/api/sessions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to create session');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function endSession(sessionId: number, payload: SleepSessionUpdate): Promise<SleepSessionRead> {
|
||||
const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}/end`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to end session');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteSession(sessionId: number): Promise<void> {
|
||||
const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to delete session');
|
||||
}
|
||||
|
||||
export async function updateSession(sessionId: number, payload: SleepSessionUpdate): Promise<SleepSessionRead> {
|
||||
const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to update session');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
|
||||
export function localToUTC(localValue: string): string {
|
||||
return new Date(localValue).toISOString();
|
||||
}
|
||||
|
||||
async function fetchStats(endpoint: string, from_date: string, to_date: string, extraParams: Record<string, string> = {}): Promise<any> {
|
||||
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 response = await fetch(`${BASE_URL}/api/stats/${endpoint}?${params.toString()}`);
|
||||
if (!response.ok) throw new Error(`Failed to fetch ${endpoint}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function fetchDailySummary(from_date: string, to_date: string, day_start: string, day_end: string): Promise<any> {
|
||||
return fetchStats('daily-summary', from_date, to_date, { day_start, day_end });
|
||||
}
|
||||
|
||||
export async function fetchTimetable(from_date: string, to_date: string, day_start: string, day_end: string): Promise<any> {
|
||||
return fetchStats('timetable', from_date, to_date, { day_start, day_end });
|
||||
}
|
||||
|
||||
export async function fetchWakeSleepTimes(from_date: string, to_date: string, wakeup_ref: string, bedtime_ref: string): Promise<any> {
|
||||
return fetchStats('wake-sleep-times', from_date, to_date, {
|
||||
wakeup_ref: localHHMMToUTCHHMM(wakeup_ref),
|
||||
bedtime_ref: localHHMMToUTCHHMM(bedtime_ref)
|
||||
});
|
||||
}
|
||||
|
||||
// Session API functions
|
||||
|
||||
@@ -28,20 +28,20 @@ const BedtimePlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_
|
||||
return;
|
||||
}
|
||||
|
||||
const data = wakeupBedtime.days.map((day) => {
|
||||
const daysAgo = Math.floor(
|
||||
(new Date(getToday()).getTime() - new Date(day.date).getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
const offsetFromRef = day.bedtime_minutes !== null ? getLocalOffsetFromReference(day.bedtime_minutes, bedtime_ref) : 0;
|
||||
return {
|
||||
date: daysAgo === 0 ? 'Today' : `${daysAgo}d ago`,
|
||||
minutes: offsetFromRef,
|
||||
time:
|
||||
day.bedtime_minutes !== null
|
||||
? formatMinutesAfter(offsetFromRef, bedtime_ref)
|
||||
: 'N/A',
|
||||
};
|
||||
}).reverse();
|
||||
const data = wakeupBedtime.days.map((day) => {
|
||||
const daysAgo = Math.floor(
|
||||
(new Date(getToday()).getTime() - new Date(day.date).getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
const offsetFromRef = day.bedtime_minutes ?? 0;
|
||||
return {
|
||||
date: daysAgo === 0 ? 'Today' : `${daysAgo}d ago`,
|
||||
minutes: offsetFromRef,
|
||||
time:
|
||||
day.bedtime_minutes !== null
|
||||
? formatMinutesAfter(offsetFromRef, bedtime_ref)
|
||||
: 'N/A',
|
||||
};
|
||||
}).reverse();
|
||||
|
||||
setChartData(data);
|
||||
}, [wakeupBedtime, bedtime_ref]);
|
||||
|
||||
@@ -28,25 +28,22 @@ const WakeupPlot: React.FC<WakeupBedtimeProps> = ({ from_date, to_date, wakeup_r
|
||||
return;
|
||||
}
|
||||
|
||||
const data = wakeupBedtime.days.map((day) => {
|
||||
const daysAgo = Math.floor(
|
||||
(new Date(getToday()).getTime() - new Date(day.date).getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
const data = wakeupBedtime.days.map((day) => {
|
||||
const daysAgo = Math.floor(
|
||||
(new Date(getToday()).getTime() - new Date(day.date).getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
let offsetFromRef = 0;
|
||||
if (day.wakeup_minutes !== null) {
|
||||
offsetFromRef = getLocalOffsetFromReference(day.wakeup_minutes, wakeup_ref);
|
||||
}
|
||||
const offsetFromRef = day.wakeup_minutes ?? 0;
|
||||
|
||||
return {
|
||||
date: daysAgo === 0 ? 'Today' : `${daysAgo}d ago`,
|
||||
minutes: offsetFromRef,
|
||||
time:
|
||||
day.wakeup_minutes !== null
|
||||
? formatMinutesAfter(offsetFromRef, wakeup_ref)
|
||||
: 'N/A',
|
||||
};
|
||||
}).reverse();
|
||||
return {
|
||||
date: daysAgo === 0 ? 'Today' : `${daysAgo}d ago`,
|
||||
minutes: offsetFromRef,
|
||||
time:
|
||||
day.wakeup_minutes !== null
|
||||
? formatMinutesAfter(offsetFromRef, wakeup_ref)
|
||||
: 'N/A',
|
||||
};
|
||||
}).reverse();
|
||||
|
||||
setChartData(data);
|
||||
}, [wakeupBedtime, wakeup_ref]);
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
# Plan: Fix Timezone issues in WakeupPlot
|
||||
# Plan: Fix Wakeup Plot Timezone Display
|
||||
|
||||
## Status: COMPLETED
|
||||
|
||||
## 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).
|
||||
`WakeupPlot.tsx` shows incorrect wakeup times (e.g., showing 01:00 instead of 05:00 when input is 03:00 UTC in Stockholm). This was caused by sending a local reference time to the backend, which then calculated a delta relative to that time being treated as UTC.
|
||||
|
||||
## Goal
|
||||
Align `WakeupPlot.tsx` with `BedtimePlot.tsx` by using the existing `getLocalOffsetFromReference` utility to correctly apply client-side timezone adjustments.
|
||||
## Solution
|
||||
1. **Frontend API Fix**: Modified `fetchWakeSleepTimes` in `frontend/src/api/client.ts` to convert the local `wakeup_ref` and `bedtime_ref` HH:MM strings into UTC HH:MM strings before sending them to the backend.
|
||||
2. **Frontend Component Fix**: Updated `WakeupPlot.tsx` and `BedtimePlot.tsx` to use the returned `wakeup_minutes`/`bedtime_minutes` (which is now a correctly calculated timezone-invariant delta) directly as the offset from reference, instead of attempting to re-adjust it using local timezone offsets.
|
||||
|
||||
## 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.
|
||||
|
||||
## Files to change
|
||||
- `frontend/src/components/stats/WakeupPlot.tsx`
|
||||
## Verification
|
||||
Verified with Playwright test in `Europe/Stockholm` timezone: 03:00 UTC input correctly displays as 05:00.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import datetime
|
||||
|
||||
def verify_real_wakeup_with_mock_api():
|
||||
# --- DATA FROM DATABASE (REAL) ---
|
||||
# session id 1 wakeup: 2026-05-27 03:17:00 UTC
|
||||
wakeup_utc = datetime.datetime(2026, 5, 27, 3, 17)
|
||||
wakeup_utc_mins = wakeup_utc.hour * 60 + wakeup_utc.minute
|
||||
|
||||
# --- SETUP PARAMETERS (CLIENT SIDE) ---
|
||||
# User is in Stockholm (+2)
|
||||
offset_in_minutes = -120
|
||||
local_ref_str = "04:00"
|
||||
local_ref_mins = 4 * 60
|
||||
|
||||
print(f"1. Database Wakeup (UTC): {wakeup_utc} ({wakeup_utc_mins}m)")
|
||||
print(f"2. Client Local Reference: {local_ref_str} ({local_ref_mins}m)")
|
||||
print(f"3. Client Timezone Offset: {offset_in_minutes}m")
|
||||
|
||||
# --- STEP 1: SIMULATE API CALL (CLIENT SIDE) ---
|
||||
def localHHMMToUTCHHMM(hhmm, offset):
|
||||
[h, m] = map(int, hhmm.split(":"))
|
||||
total_mins_utc = (h * 60 + m) + offset
|
||||
norm = ((total_mins_utc % 1440) + 1440) % 1440
|
||||
return f"{norm // 60:02d}:{norm % 60:02d}"
|
||||
|
||||
utc_ref = localHHMMToUTCHHMM(local_ref_str, offset_in_minutes)
|
||||
print(f"4. Simulated API call with wakeup_ref (converted to UTC): {utc_ref}")
|
||||
|
||||
# --- STEP 2: SIMULATE BACKEND (SERVER SIDE) ---
|
||||
# Backend receives utc_ref and calculates delta: wakeup_utc - utc_ref
|
||||
utc_ref_mins = (int(utc_ref.split(":")[0]) * 60 + int(utc_ref.split(":")[1]))
|
||||
backend_delta = wakeup_utc_mins - utc_ref_mins
|
||||
print(f"5. Backend returns delta: {backend_delta}")
|
||||
|
||||
# --- STEP 3: SIMULATE COMPONENT RENDERING (CLIENT SIDE) ---
|
||||
# Component receives delta and uses it directly as offsetFromRef.
|
||||
offset_from_ref = backend_delta
|
||||
|
||||
def formatMinutesAfter(minutes, ref):
|
||||
[rh, rm] = map(int, ref.split(":"))
|
||||
target = (rh * 60 + rm) + minutes
|
||||
norm = ((target % 1440) + 1440) % 1440
|
||||
return f"{norm // 60:02d}:{norm % 60:02d}"
|
||||
|
||||
displayed_time = formatMinutesAfter(offset_from_ref, local_ref_str)
|
||||
print(f"6. Component renders time: {displayed_time}")
|
||||
|
||||
# --- ASSERTION ---
|
||||
expected_local_time = "05:17"
|
||||
print(f"\nASSERTION: Expected {expected_local_time}, Actual {displayed_time}")
|
||||
if displayed_time == expected_local_time:
|
||||
print("VERIFICATION SUCCESSFUL!")
|
||||
else:
|
||||
print("VERIFICATION FAILED!")
|
||||
exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
verify_real_wakeup_with_mock_api()
|
||||
@@ -0,0 +1,89 @@
|
||||
import datetime
|
||||
|
||||
def solve():
|
||||
# --- CONSTANTS FROM DATABASE/USER ---
|
||||
# Actual wakeup time (UTC) from DB for session 1
|
||||
wakeup_utc_str = "2026-05-27 03:17:00"
|
||||
wakeup_utc = datetime.datetime.strptime(wakeup_utc_str, "%Y-%m-%d %H:%M:%S")
|
||||
wakeup_utc_mins = wakeup_utc.hour * 60 + wakeup_utc.minute
|
||||
|
||||
# User's local reference time (Stockholm, UTC+2)
|
||||
local_ref_str = "04:00"
|
||||
local_ref_mins = 4 * 60
|
||||
|
||||
# Stockholm offset (UTC+2) -> getTimezoneOffset() returns -120
|
||||
offset_in_minutes = -120
|
||||
|
||||
print(f"DATABASE WAKEUP (UTC): {wakeup_utc_str} ({wakeup_utc_mins} mins from midnight)")
|
||||
print(f"LOCAL REFERENCE: {local_ref_str} ({local_ref_mins} mins from midnight)")
|
||||
print(f"STOCKHOLM OFFSET: {offset_in_minutes} (getTimezoneOffset style)")
|
||||
|
||||
# --- SCENARIO 1: THE BUGGY WAY (Current implementation) ---
|
||||
# Backend receives local "04:00" but treats it as UTC.
|
||||
# Backend calculates delta = (UTC_wakeup - Ref_as_if_UTC)
|
||||
ref_as_utc = local_ref_mins # 240
|
||||
buggy_delta = wakeup_utc_mins - ref_as_utc # 197 - 240 = -43
|
||||
print(f"\n[BUGGY SCENARIO]")
|
||||
print(f"Backend returns (wrong) delta: {buggy_delta}")
|
||||
|
||||
# Frontend receives -43 and tries to adjust it.
|
||||
# offsetFromRef = (received_delta - offset) - ref_mins_local
|
||||
# wait, the logic was: localMins = (received + abs(offset)).
|
||||
# Let's follow the code exactly:
|
||||
# adjustForTimezone(delta) = delta - offset_in_minutes
|
||||
# (Wait, if the component thinks 'delta' is absolute minutes from midnight?)
|
||||
# The user said: "Render the Wakeup Times given -43 as value ... results to 01:17"
|
||||
# Let's see how to get 01:17 from -43 and ref 04:00.
|
||||
# 240 (ref) - 163 = 77? No.
|
||||
# If offsetFromRef is -163: formatMinutesAfter(-163, "04:00") -> 240 - 163 = 77 (01:17).
|
||||
# How do we get -163 from -43?
|
||||
# -43 - 120 = -163. (Ah! It's subtracting the absolute offset instead of handling sign).
|
||||
# Or: -43 + (-120) = -163.
|
||||
# This happens if the code does: delta + offset_in_minutes (where offset is -120).
|
||||
# Wait, if offset is -120: -43 + (-120) = -163.
|
||||
# So the component is doing: localMins = delta + offset_in_minutes.
|
||||
# Then offsetFromRef = localMins - ref_mins = (-163) - 240 = -403? No.
|
||||
# Let's check component:
|
||||
# offsetFromRef = getLocalOffsetFromReference(day.wakeup_minutes, wakeup_ref);
|
||||
# which is: (wakeup_minutes - offsetInMinutes) - ref_mins.
|
||||
# If wakeup_minutes = -43 and offsetInMinutes = 120 (if sign is flipped):
|
||||
# (-43 - 120) - 240 = -163 - 240 = -403.
|
||||
# If offsetInMinutes = -120:
|
||||
# (-43 - (-120)) - 240 = (77) - 240 = -163.
|
||||
# YES! That's exactly it.
|
||||
# (-43 - (-120)) - 240 = -163.
|
||||
# formatMinutesAfter(-163, "04:00") -> 240 + (-163) = 77.
|
||||
# 77 minutes is 01:17. Correct!
|
||||
|
||||
def buggy_frontend(delta, ref_mins, offset):
|
||||
local_m = delta - offset
|
||||
return local_m - ref_mins
|
||||
|
||||
buggy_offset_from_ref = buggy_frontend(buggy_delta, local_ref_mins, offset_in_minutes)
|
||||
print(f"BUGGY Frontend offsetFromRef: {buggy_offset_from_ref}")
|
||||
print(f"BUGGY Frontend Display: {local_ref_mins + buggy_offset_from_ref} (Minutes from midnight)")
|
||||
|
||||
# --- SCENARIO 2: THE FIXED WAY (My proposed plan) ---
|
||||
# 1. Frontend converts local ref "04:00" to UTC "02:00".
|
||||
# 2. Backend receives ref=120. Calculates delta = 197 - 120 = 77.
|
||||
# 3. Frontend receives delta=77 and uses it DIRECTLY as offsetFromRef.
|
||||
|
||||
correct_delta = wakeup_utc_mins - (local_ref_mins + offset_in_minutes)
|
||||
print(f"\n[FIXED SCENARIO]")
|
||||
print(f"Backend returns (correct) delta: {correct_delta}")
|
||||
|
||||
# Frontend uses it directly.
|
||||
fixed_offset_from_ref = correct_delta
|
||||
print(f"FIXED Frontend offsetFromRef: {fixed_offset_from_ref}")
|
||||
print(f"FIXED Frontend Display: {local_ref_mins + fixed_offset_from_ref} (Minutes from midnight)")
|
||||
|
||||
# ASSERT
|
||||
expected_mins = 5 * 60 + 17 # 317
|
||||
actual_mins = local_ref_mins + fixed_offset_from_ref
|
||||
print(f"\nASSERTION: Expected {expected_mins}, Actual {actual_mins}")
|
||||
if actual_mins == expected_mins:
|
||||
print("SUCCESS!")
|
||||
else:
|
||||
print("FAILURE!")
|
||||
|
||||
solve()
|
||||
@@ -0,0 +1,57 @@
|
||||
import datetime
|
||||
|
||||
def solve():
|
||||
# Scenario: User in Stockholm (UTC+2)
|
||||
# Local Wakeup Reference: 04:00
|
||||
# Actual Wakeup Time (UTC): 03:17
|
||||
|
||||
offset_minutes = -120 # Stockholm UTC+2 -> getTimezoneOffset() is -120
|
||||
|
||||
wakeup_ref_local = "04:00"
|
||||
wakeup_ref_mins_local = 4 * 60
|
||||
|
||||
# Let's assume the backend calculates: wakeup_minutes = (UTC_wakeup - UTC_ref)
|
||||
# where it treats the provided ref as if it were in UTC.
|
||||
|
||||
# Case 1: BUGGY (Frontend sends Local ref "04:00" to backend)
|
||||
# Backend treats 04:00 as UTC.
|
||||
utc_wakeup = 3 * 60 + 17 # 03:17 UTC
|
||||
wakeup_minutes_from_backend = utc_wakeup - wakeup_ref_mins_local # 197 - 240 = -43
|
||||
print(f"BUGGY: Backend returns wakeup_minutes (assuming ref 04:00 is UTC): {wakeup_minutes_from_backend}")
|
||||
|
||||
# Frontend (current) tries to fix it with adjustForTimezone:
|
||||
received_wakeup_minutes = wakeup_minutes_from_backend
|
||||
# offsetFromRef = getLocalOffsetFromReference(received, "04:00")
|
||||
# localMins = adjustForTimezone(received) = received - offset_minutes
|
||||
# offsetFromRef = localMins - ref_mins_local
|
||||
|
||||
def adjustForTimezone(utc_min):
|
||||
return utc_min - offset_minutes
|
||||
|
||||
def getLocalOffsetFromReference(utc_min, ref_mins):
|
||||
local_m = adjustForTimezone(utc_min)
|
||||
return local_m - ref_mins
|
||||
|
||||
offset_from_ref = getLocalOffsetFromReference(received_wakeup_minutes, wakeup_ref_mins_local)
|
||||
print(f"BUGGY: Frontend calculates offsetFromRef: {offset_from_ref}")
|
||||
# formatMinutesAfter(offset, ref) -> ref + offset
|
||||
print(f"BUGGY: Displayed time (ref + offset): {wakeup_ref_mins_local + offset_from_ref}")
|
||||
|
||||
# Case 2: CORRECT (Frontend sends UTC ref "02:00" to backend)
|
||||
# Stockholm +2 means 04:00 Local is 02:00 UTC.
|
||||
utc_wakeup_ref = wakeup_ref_mins_local + offset_minutes # 240 - 120 = 120
|
||||
wakeup_minutes_from_backend_correct = utc_wakeup - utc_wakeup_ref # 197 - 120 = 77
|
||||
print(f"\nCORRECT: Backend returns wakeup_minutes (assuming ref 02:00 is UTC): {wakeup_minutes_from_backend_correct}")
|
||||
|
||||
# Frontend receives 77.
|
||||
# If it's a delta, the component should use it directly as offsetFromRef.
|
||||
# Or if we want to keep existing logic, we have to realize 77 is ALREADY the delta.
|
||||
# But the component thinks it's "minutes from midnight".
|
||||
# This is a mismatch.
|
||||
|
||||
print("---")
|
||||
print(f"Target Local Wakeup: 05:17 ({3 * 60 + 17 + abs(offset_minutes)})")
|
||||
print(f"Target Local Ref: 04:00 ({wakeup_ref_mins_local})")
|
||||
print(f"Target Local Delta: {317 - 240} = 77")
|
||||
|
||||
solve()
|
||||
Reference in New Issue
Block a user