5 Commits

Author SHA1 Message Date
jacobh2 81eadf618a Update gitignore
Build Frontend / build (push) Successful in 9m6s
2026-06-04 11:33:42 +02:00
jacobh2 6847886e7a chore: add vite environment type definitions 2026-06-04 11:33:21 +02:00
jacobh2 5d8a8b3c57 chore: update deployment and app structure 2026-06-04 11:31:44 +02:00
jacobh2 32d3e86d38 fix(frontend): resolve linting errors and component duplication 2026-06-04 11:30:18 +02:00
jacobh2 b763aa1e9d Small cleanup
Build Backend / build (push) Successful in 1m54s
2026-06-04 09:18:22 +02:00
22 changed files with 64 additions and 244 deletions
+2
View File
@@ -51,6 +51,8 @@ jobs:
run: |
docker buildx build \
--platform linux/arm64 \
--build-arg VITE_APP_VERSION=$(git describe --tags) \
--build-arg VITE_APP_GITSHA=$(git rev-parse --short HEAD) \
-t gitea.lan/jacobh2/babymonitor-frontend:${{ env.VERSION }} \
./frontend \
--push
+2
View File
@@ -5,6 +5,8 @@
*.js
*.jsx
.omo/
### Node ###
# Logs
logs
-14
View File
@@ -1,14 +0,0 @@
INFO: Will watch for changes in these directories: ['/Users/jhag/projects/babymonitor/backend']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [63655] using StatReload
INFO: Started server process [63657]
INFO: Waiting for application startup.
INFO: Application startup complete.
WARNING: StatReload detected changes in 'app/main.py'. Reloading...
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [63657]
INFO: Started server process [71064]
INFO: Waiting for application startup.
INFO: Application startup complete.
-16
View File
@@ -1,16 +0,0 @@
#!/bin/bash
docker buildx build \
--platform linux/arm64 \
-t babymonitor-frontend:latest \
-f frontend/Dockerfile \
--load \
frontend
docker buildx build \
--platform linux/arm64 \
-t babymonitor-backend:latest \
-f backend/Dockerfile \
--load \
backend
+4
View File
@@ -1,5 +1,9 @@
FROM node:22-alpine AS build
ARG VITE_APP_VERSION=unknown
ARG VITE_APP_GITSHA=unknown
ENV VITE_APP_VERSION=$VITE_APP_VERSION
ENV VITE_APP_GITSHA=$VITE_APP_GITSHA
WORKDIR /app
# Copy package files first to leverage Docker cache
+3
View File
@@ -4,6 +4,8 @@ import TrackerPage from './components/tracker/TrackerPage';
import StatsPage from './components/stats/StatsPage';
import Navbar from './components/Navbar';
import Playground from './components/Playground';
import Footer from './components/Footer';
function App() {
return (
@@ -17,6 +19,7 @@ function App() {
<Route path="/playground" element={<Playground />} />
</Routes>
</main>
<Footer />
</div>
</Router>
);
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
const Footer: React.FC = () => {
return (
<footer className="py-4 text-center">
<p className="text-xs text-slate-500">
v{__APP_VERSION__} ({__APP_GITSHA__})
</p>
</footer>
);
};
export default Footer;
@@ -100,8 +100,8 @@ const DailySummary: React.FC<DailySummaryProps> = ({ from_date, to_date, day_sta
width={60}
/>
<Tooltip
formatter={(value: any, name: any) => [
formatMinutes(Math.round(Number(value ?? 0) * 60)),
formatter={(value: number | string | readonly (string | number)[] | undefined, name: string | number | undefined) => [
formatMinutes(Math.round(Number((Array.isArray(value) ? value[0] : value ?? 0) * 60))),
name === 'daySleep' ? 'Day' : 'Night',
]}
/>
@@ -52,7 +52,8 @@ const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) =
useEffect(() => {
if (days.length > 0) {
setSelectedDayIndex(days.length - 1);
const newIndex = days.length - 1;
setSelectedDayIndex(prev => (prev !== newIndex ? newIndex : prev));
}
}, [days]);
@@ -158,7 +159,7 @@ const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) =
</button>
);
})}
</div >
</div>
)}
{/* Grid Container */}
@@ -176,27 +177,27 @@ const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) =
{visibleDays.map((day) => (
<div key={day} className="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-2">
{getDayName(day)}
</div >
</div>
))}
</div >
</div>
{/* Time Grid Container */}
<div className="relative">
<div
className="grid"
style={{
gridTemplateColumns: tier === 'desktop'
? `4rem repeat(${days.length}, minmax(40px, 1fr))`
: `4rem repeat(${visibleDays.length}, 1fr)`
}}
>
className="grid"
style={{
gridTemplateColumns: tier === 'desktop'
? `4rem repeat(${days.length}, minmax(40px, 1fr))`
: `4rem repeat(${visibleDays.length}, 1fr)`
}}
>
{/* Time Rows */}
{timeSteps.map((step) => (
<Fragment key={step}>
{/* Time Label */}
<div className="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-2">
{step % 60 === 0 ? formatTimeLabel(step) : ''}
</div >
</div>
{/* Grid Cells */}
{visibleDays.map((day) => {
@@ -220,11 +221,11 @@ const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) =
transform: 'translateY(-50%)'
}}
/>
</div >
</div >
</div >
</div >
</div>
</div>
</div>
</div>
);
};
export default Timetable;
export default Timetable;
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { createContext, useState, useEffect, ReactNode } from 'react';
const STORAGE_KEY = 'baby-sleep-settings';
+1 -1
View File
@@ -1,5 +1,5 @@
import { useContext } from 'react';
import { SettingsContext, Settings } from './SettingsProvider';
import { SettingsContext } from './SettingsProvider';
export function useSettings() {
const context = useContext(SettingsContext);
+4
View File
@@ -0,0 +1,4 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string;
declare const __APP_GITSHA__: string;
+7
View File
@@ -3,9 +3,16 @@ import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
const APP_VERSION = process.env.VITE_APP_VERSION || 'unknown';
const APP_GITSHA = process.env.VITE_APP_GITSHA || 'unknown';
export default defineConfig({
base: './',
plugins: [react(), tailwindcss()],
define: {
__APP_VERSION__: JSON.stringify(APP_VERSION),
__APP_GITSHA__: JSON.stringify(APP_GITSHA),
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
-5
View File
@@ -1,5 +0,0 @@
podman save babymonitor-frontend:latest -o frontend.tar
podman save babymonitor-backend:latest -o backend.tar
scp frontend.tar backend.tar jacobhagstedt@raspberrypi.local:~/
-4
View File
@@ -1,4 +0,0 @@
{
"status": "failed",
"failedTests": []
}
-33
View File
@@ -1,33 +0,0 @@
const adjustForTimezone = (utcMinutes) => {
const offsetInMinutes = -120; // Simulate UTC+2
return utcMinutes - offsetInMinutes;
};
const formatMinutesAfter = (minutes, reference = "04:00") => {
const [refHours, refMinutes] = reference.split(":").map(Number);
const totalRefMinutes = refHours * 60 + refMinutes;
const targetMinutes = totalRefMinutes + minutes;
const normalizedMinutes = ((targetMinutes % 1440) + 1440) % 1440;
const hours = Math.floor(normalizedMinutes / 60);
const mins = normalizedMinutes % 60;
return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
};
// Case 1: Backend sends offset from 04:00 UTC (-5 minutes)
const backendOffset = -5;
const adjusted = adjustForTimezone(backendOffset);
console.log("Adjusted offset:", adjusted); // Should be 115
console.log("Formatted time:", formatMinutesAfter(adjusted, "04:00")); // Should be 05:55
// Case 2: Backend sends minutes from UTC midnight (235 minutes)
const backendMidnightMins = 235;
const adjusted2 = adjustForTimezone(backendMidnightMins);
console.log("Adjusted midnight mins:", adjusted2); // Should be 355
// If we use it as an offset from reference:
console.log("Formatted time (as offset):", formatMinutesAfter(adjusted2, "04:00")); // Should be 09:55 (WRONG if it was supposed to be 05:55)
// If we want the offset from reference for Case 2:
const refMins = 4 * 60;
const correctOffset = adjusted2 - refMins; // 355 - 240 = 115
console.log("Corrected offset (from midnight):", correctOffset); // 115
console.log("Formatted time (corrected):", formatMinutesAfter(correctOffset, "04:00")); // 05:55
+7 -5
View File
@@ -19,10 +19,10 @@ exitcode '1': failure
```
- [ ] Editing a sleep session in "sleep sessions", when I click the edit button the times show up as UTC but should show as my local timezone
- [ ] We should drop the "notes" column from the sleep_session table
- [ ] Switch over to oh-my-opencode and try it out
- [ ] Adda footer at the bottom of the app that shows the version (aka git tag) of the app
- [ ] Fix race condition of gitea workflows
- [/] We should drop the "notes" column from the sleep_session table
- [x] Switch over to oh-my-opencode and try it out
- [x] Adda footer at the bottom of the app that shows the version (aka git tag) of the app
- [-] Fix race condition of gitea workflows
If both workflows run at the same time, a race condition occurs:
1. Collision: Both workflows will query the latest tag (e.g., v1.0.5) and calculate the same next version (v1.0.6).
@@ -30,4 +30,6 @@ If both workflows run at the same time, a race condition occurs:
3. The Loser: The second workflow will attempt to push v1.0.6, but the remote will reject it because that tag already exists, causing the workflow to fail.
This happens because both workflows share the same global tag namespace (v1.0.x). To fix this, you should differentiate them by using unique prefixes, such as backend-v1.0.x and frontend-v1.0.x.
- [ ] remove the lint form the gitea workflows added by the agent. makes no sense there
--> Invalid. the containers are named differently.
- [x] remove the lint form the gitea workflows added by the agent. makes no sense there
-58
View File
@@ -1,58 +0,0 @@
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()
-57
View File
@@ -1,57 +0,0 @@
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()
-31
View File
@@ -1,31 +0,0 @@
# Plan: Fix Timezone Handling in Wakeup Times Module
## Problem Description
The "Wakeup Times" chart (in `frontend/src/components/stats/WakeupPlot.tsx`) displays wakeup times incorrectly due to a timezone mismatch.
- **Observation**: If a baby wakes up at `05:55` local time (e.g., in UTC+2), the chart shows `03:55`.
- **Cause**: The backend stores times in UTC. The frontend currently receives minutes relative to UTC midnight and renders them directly using `formatMinutesAfter` without applying the client's local timezone offset.
## Investigation Steps
1. **Backend Verification**:
- Examine `backend/app/routers/stats.py` (or equivalent) and the CRUD logic to confirm that `wakeup_minutes` is calculated based on UTC midnight.
- Verify how the backend handles the `wakeup_ref` and `bedtime_ref` parameters.
2. **Frontend Logic Audit**:
- Trace the data flow from `fetchWakeSleepTimes` -> `useStats` -> `WakeupPlot.tsx`.
- Confirm that `wakeup_minutes` is indeed a raw offset from UTC midnight.
## Implementation Plan
1. **Calculate Local Offset**: Use `new Date().getTimezoneOffset()` in the frontend to determine the difference between UTC and local time.
2. **Adjust Minutes**:
- In `WakeupPlot.tsx`, modify the mapping logic to adjust `day.wakeup_minutes` by the local timezone offset before it is passed to `formatMinutesAfter`.
- Formula: `local_minutes = utc_minutes - (timezone_offset_in_minutes)`.
*(Note: `getTimezoneOffset()` returns minutes positive for west of UTC, negative for east, so subtraction is correct to add offset to UTC).*
3. **Refactor Utilities (if needed)**: If the logic is used elsewhere, consider moving timezone-aware formatting to `frontend/src/utils/time.ts`.
## Verification Plan
1. **Manual Verification**:
- Mock a response from the backend with a known UTC time.
- Check if the rendered string matches the expected local time.
2. **Automated Testing**:
- Add/update a test case in `frontend/tests/e2e/stats.spec.ts` (or a unit test in `frontend/tests/`) that simulates different timezone offsets to ensure the display is correct.
3. **Regression Check**:
- Ensure `BedtimePlot.tsx` and other time-related components are not negatively impacted by changes to shared utilities.