Working on fixing sleep timetable & add gitea workflows

This commit is contained in:
2026-05-20 17:30:13 +02:00
parent e38fae1940
commit b361fc5172
9 changed files with 182 additions and 171 deletions
@@ -1,61 +0,0 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import React, { useMemo } from 'react';
import { useStats } from '../../hooks/useStats';
import { timeToMinutes } from '../../utils/time';
const Timetable = ({ from_date, to_date, day_start, day_end }) => {
const { timetable, isLoading } = useStats(from_date, to_date, day_start, day_end);
const days = useMemo(() => {
const start = new Date(from_date);
const end = new Date(to_date);
const result = [];
let current = new Date(start);
while (current <= end) {
result.push(current.toISOString().split('T')[0]);
current.setDate(current.getDate() + 1);
}
return result;
}, [from_date, to_date]);
const timeSteps = useMemo(() => {
return Array.from({ length: 24 }, (_, i) => i * 60);
}, []);
if (isLoading)
return _jsx("div", { className: "text-center p-4", children: "Loading timetable..." });
if (!timetable?.slots || timetable.slots.length === 0) {
return _jsx("div", { className: "text-center p-4 text-gray-500", children: "No data available for this period." });
}
const getDayName = (dateStr) => {
return new Date(dateStr).toLocaleDateString('en-US', { weekday: 'short' });
};
const formatTimeLabel = (mins) => {
const hours = Math.floor(mins / 60);
const minutes = mins % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
};
const isSleepingInInterval = (date, intervalStart, intervalEnd) => {
return timetable.slots.some((slot) => {
if (slot.date !== date || !slot.sleeping)
return false;
let sStart = timeToMinutes(slot.slot_start);
let sEnd = timeToMinutes(slot.slot_end);
if (sEnd <= sStart) {
const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd);
const overlap2 = Math.max(0, intervalStart) < Math.min(sEnd, intervalEnd);
return overlap1 || overlap2;
}
return Math.max(sStart, intervalStart) < Math.min(sEnd, intervalEnd);
});
};
const currentMins = new Date().getHours() * 60 + new Date().getMinutes();
return (_jsxs("div", { className: "bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto", children: [_jsx("h3", { className: "text-lg font-semibold mb-4 text-gray-800", children: "Sleep Timetable" }), _jsxs("div", { className: "min-w-[600px] flex flex-col gap-px", children: [_jsxs("div", { className: "grid", style: {
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}, children: [_jsx("div", { className: "h-8" }), " ", days.map((day) => (_jsx("div", { className: "text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8", children: getDayName(day) }, day)))] }), _jsx("div", { className: "relative", children: _jsxs("div", { className: "grid", style: {
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}, children: [timeSteps.map((step) => (_jsxs(React.Fragment, { children: [_jsx("div", { className: "text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8", children: step % 120 === 0 ? formatTimeLabel(step) : '' }), days.map((day) => {
const sleeping = isSleepingInInterval(day, step, step + 60);
return (_jsx("div", { className: `h-8 w-full ${sleeping ? 'bg-green-500' : 'bg-gray-50'}` }, `${day}-${step}`));
})] }, step))), _jsx("div", { className: "absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10", style: {
top: `${(currentMins / 1440) * 100}%`,
transform: 'translateY(-50%)'
} })] }) })] })] }));
};
export default Timetable;
@@ -49,16 +49,14 @@ describe('SleepTimetable', () => {
expect(screen.getByText(/SUN/i)).toBeInTheDocument(); // 2025-04-20 is Sunday
expect(screen.getByText(/MON/i)).toBeInTheDocument(); // 2025-04-21 is Monday
// Check for time labels (every 2 hours: 00, 02, 04...)
// Check for time labels (every 1 hour: 00, 01, 02...)
expect(screen.getByText('00:00')).toBeInTheDocument();
expect(screen.getByText('01:00')).toBeInTheDocument();
expect(screen.getByText('02:00')).toBeInTheDocument();
expect(screen.getByText('03:00')).toBeInTheDocument();
expect(screen.getByText('04:00')).toBeInTheDocument();
expect(screen.getByText('22:00')).toBeInTheDocument();
// Verify non-labeled hours (every hour that isn't %120 == 0)
// We can't easily check for empty text nodes in Vitest/RTL without more precision
// but we can check that 01:00 is NOT present as a text label
expect(screen.queryByText('01:00')).not.toBeInTheDocument();
expect(screen.getByText('23:00')).toBeInTheDocument();
});
it('colors sleeping intervals correctly', () => {
@@ -13,12 +13,12 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
const { timetable, isLoading } = useStats(from_date, to_date, day_start, day_end);
const days = useMemo(() => {
const start = new Date(from_date);
const end = new Date(to_date);
const start = new Date(from_date + 'T00:00:00');
const end = new Date(to_date + 'T23:59:59');
const result: string[] = [];
let current = new Date(start);
while (current <= end) {
result.push(current.toISOString().split('T')[0]);
result.push(current.toLocaleDateString('en-CA'));
current.setDate(current.getDate() + 1);
}
return result;
@@ -51,6 +51,7 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
let sEnd = timeToMinutes(slot.slot_end);
if (sEnd <= sStart) {
// Handle wrap-around within a single slot if it happens (though unlikely for 15m segments)
const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd);
const overlap2 = Math.max(0, intervalStart) < Math.min(sEnd, intervalEnd);
return overlap1 || overlap2;
@@ -94,7 +95,7 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
<React.Fragment key={step}>
{/* Time Label */}
<div className="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8">
{step % 120 === 0 ? formatTimeLabel(step) : ''}
{step % 60 === 0 ? formatTimeLabel(step) : ''}
</div>
{/* Grid Cells */}
@@ -110,20 +111,20 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
</React.Fragment>
))}
{/* Red Marker Line */}
<div
className="absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10"
style={{
top: `${(currentMins / 1440) * 100}%`,
transform: 'translateY(-50%)'
}}
/>
</div>
</div>
</div>
</div>
);
{/* Red Marker Line */}
<div
className="absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10"
style={{
top: `${(currentMins / 1440) * 100}%`,
transform: 'translateY(-50%)'
}}
/>
</div>
</div>
</div>
</div>
);
};
export default Timetable;
+4 -2
View File
@@ -12,8 +12,10 @@ import { useSettings } from '../../hooks/useSettings';
const StatsPage: React.FC = () => {
const { settings, update } = useSettings();
const from_date = new Date(new Date().setDate(new Date().getDate() - settings.statsInterval)).toISOString().split('T')[0];
const to_date = new Date().toISOString().split('T')[0];
const now = new Date();
const to_date = now.toLocaleDateString('en-CA'); // YYYY-MM-DD in local time
const from_date = new Date(now.setDate(now.getDate() - settings.statsInterval)).toLocaleDateString('en-CA');
return (
<div className="max-w-4xl mx-auto space-y-8 pb-12">