Files
baby-monitor/src/baby_monitor/static/sleep.html
T
Brian Bjarke Jensen 5b17508ade
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m11s
Python Code Quality / python-code-quality (pull_request) Successful in 11s
Python Test / python-test (pull_request) Successful in 19s
CQ fixes
2025-12-03 19:49:48 +01:00

1140 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sleep - Baby Monitor</title>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: #f0f0f0;
min-height: 100vh;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.button {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition:
transform 0.2s,
box-shadow 0.2s;
text-decoration: none;
display: inline-block;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(79, 172, 254, 0.4);
}
.button.secondary {
background: #6c757d;
margin-left: 10px;
}
.button.secondary:hover {
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.4);
}
.hidden {
display: none;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 20px;
}
.empty-state h2 {
color: #333;
margin-bottom: 10px;
}
.empty-state p {
margin-bottom: 30px;
}
.chart-container {
margin-bottom: 40px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.chart-container h2 {
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
.chart-wrapper {
position: relative;
height: 300px;
max-width: 100%;
margin: 0 auto;
}
.chart-wrapper canvas {
max-width: 100%;
max-height: 100%;
}
.charts-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 40px;
}
@media (max-width: 768px) {
.charts-row {
grid-template-columns: 1fr;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card h3 {
color: #666;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 10px;
}
.stat-card .value {
color: #4facfe;
font-size: 32px;
font-weight: 700;
}
.list-container {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
}
.list-container h2 {
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
.sleep-list {
max-height: 500px;
overflow-y: auto;
}
.sleep-item {
background: white;
padding: 16px;
border-radius: 6px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.sleep-item:last-child {
margin-bottom: 0;
}
.sleep-info {
flex: 1;
}
.sleep-child {
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.sleep-time {
color: #666;
font-size: 14px;
margin-bottom: 4px;
}
.sleep-duration {
color: #4facfe;
font-size: 14px;
font-weight: 600;
}
.sleep-actions {
display: flex;
gap: 8px;
}
.edit-button {
background: #4facfe;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.edit-button:hover {
background: #3a9be8;
}
.controls {
margin-bottom: 30px;
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.controls label {
font-weight: 600;
color: #333;
}
.controls select {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
background: white;
}
.controls select:focus {
outline: none;
border-color: #4facfe;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-right: 8px;
}
.badge.ongoing {
background: #4facfe;
color: white;
}
</style>
</head>
<body>
<div class="container">
<h1>😴 Sleep Tracking</h1>
<p class="subtitle">Monitor your baby's sleep patterns</p>
<div class="controls">
<label for="childFilter">Child:</label>
<select id="childFilter">
<option value="">All Children</option>
</select>
<label for="timeRangeFilter">Time Range:</label>
<select id="timeRangeFilter">
<option value="1">Last 24 hours</option>
<option value="3">Last 3 days</option>
<option value="7" selected>Last 7 days</option>
<option value="14">Last 14 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
</div>
<div id="content" class="loading">
<p>Loading sleep data...</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="/menu.js"></script>
<script>
const token = localStorage.getItem("access_token");
// Check if user is logged in
if (!token) {
window.location.href = "/login.html";
}
// Initialize burger menu
initBurgerMenu({ includeHome: true });
let allSleeps = [];
let allChildren = [];
let selectedChildId = "";
// Filter change handlers (will be attached after children are loaded)
function attachChildFilterListener() {
const childFilter = document.getElementById("childFilter");
if (childFilter) {
childFilter.addEventListener("change", function () {
selectedChildId = this.value;
// Save selection to localStorage
if (selectedChildId) {
localStorage.setItem("lastSleepChildFilter", selectedChildId);
} else {
localStorage.removeItem("lastSleepChildFilter");
}
renderCharts();
});
}
}
function attachTimeRangeListener() {
const timeRangeFilter = document.getElementById("timeRangeFilter");
if (timeRangeFilter) {
timeRangeFilter.addEventListener("change", function () {
// Save selection to localStorage
localStorage.setItem("lastSleepTimeRange", this.value);
renderCharts();
});
}
}
async function loadData() {
try {
// Fetch children
const childrenResponse = await fetch("/api/children", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!childrenResponse.ok) {
throw new Error("Failed to fetch children");
}
allChildren = await childrenResponse.json();
// Auto-select first child if available
if (allChildren.length > 0 && !selectedChildId) {
selectedChildId = allChildren[0].id;
}
// Populate child filter or show single child
const childFilterContainer = document.querySelector(".controls");
if (allChildren.length === 1) {
// Single child: show as read-only text
const child = allChildren[0];
childFilterContainer.innerHTML = `
<label for="childDisplay">Child:</label>
<input
type="text"
id="childDisplay"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"
/>
<label for="timeRangeFilter">Time Range:</label>
<select id="timeRangeFilter">
<option value="1">Last 24 hours</option>
<option value="3">Last 3 days</option>
<option value="7" selected>Last 7 days</option>
<option value="14">Last 14 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
`;
} else {
// Multiple children: show dropdown
const childFilter = document.getElementById("childFilter");
childFilter.innerHTML = "";
allChildren.forEach((child) => {
const option = document.createElement("option");
option.value = child.id;
option.textContent = child.name;
childFilter.appendChild(option);
});
}
// Restore last selected child from localStorage
const lastChildId = localStorage.getItem("lastSleepChildFilter");
if (lastChildId && allChildren.length > 1) {
const childFilter = document.getElementById("childFilter");
childFilter.value = lastChildId;
selectedChildId = lastChildId;
}
// Restore last selected time range from localStorage
const timeRangeFilter = document.getElementById("timeRangeFilter");
const lastTimeRange = localStorage.getItem("lastSleepTimeRange");
if (lastTimeRange) {
timeRangeFilter.value = lastTimeRange;
}
// Fetch sleep logs
const sleepResponse = await fetch("/api/sleep", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!sleepResponse.ok) {
throw new Error("Failed to fetch sleep data");
}
allSleeps = await sleepResponse.json();
// Attach event listener for child filter if multiple children
if (allChildren.length > 1) {
attachChildFilterListener();
}
// Always attach time range listener after recreating the controls
attachTimeRangeListener();
renderCharts();
} catch (error) {
console.error("Error loading data:", error);
document.getElementById("content").innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">😴</div>
<h2>Error Loading Data</h2>
<p>${error.message}</p>
<button class="button" onclick="window.location.href='/'">Go Home</button>
</div>
`;
}
}
function renderCharts() {
const timeRange = parseInt(
document.getElementById("timeRangeFilter").value,
);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - timeRange);
// Filter sleeps by time range and child
let filteredSleeps = allSleeps.filter((sleep) => {
const sleepDate = new Date(sleep.start_time);
const matchesTimeRange = sleepDate >= cutoffDate;
const matchesChild =
!selectedChildId || sleep.child_id === parseInt(selectedChildId);
return matchesTimeRange && matchesChild;
});
if (filteredSleeps.length === 0) {
document.getElementById("content").innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">😴</div>
<h2>No Sleep Data</h2>
<p>Start tracking sleep sessions from the home page.</p>
<button class="button" onclick="window.location.href='/'">Go Home</button>
</div>
`;
return;
}
// Calculate stats
const completedSleeps = filteredSleeps.filter(
(sleep) => sleep.end_time,
);
const totalSessions = filteredSleeps.length;
const ongoingSessions = filteredSleeps.filter(
(sleep) => !sleep.end_time,
).length;
let totalMinutes = 0;
let longestMinutes = 0;
let shortestMinutes = Infinity;
completedSleeps.forEach((sleep) => {
const duration = calculateDuration(sleep.start_time, sleep.end_time);
totalMinutes += duration;
if (duration > longestMinutes) longestMinutes = duration;
if (duration < shortestMinutes) shortestMinutes = duration;
});
const avgMinutes =
completedSleeps.length > 0
? Math.round(totalMinutes / completedSleeps.length)
: 0;
if (completedSleeps.length === 0) {
shortestMinutes = 0;
}
// Prepare data for charts
const barChartData = prepareBarChartData(filteredSleeps, timeRange);
const durationDistData =
prepareDurationDistributionData(completedSleeps);
let html = `
<div class="stats-grid">
<div class="stat-card">
<h3>Total Sessions</h3>
<div class="value">${totalSessions}</div>
</div>
<div class="stat-card">
<h3>Avg Duration</h3>
<div class="value">${formatDuration(avgMinutes)}</div>
</div>
<div class="stat-card">
<h3>Longest Sleep</h3>
<div class="value">${formatDuration(longestMinutes)}</div>
</div>
</div>
<div class="chart-container">
<h2>Total Sleep Time (Last ${timeRange} Day${timeRange !== 1 ? "s" : ""})</h2>
<div class="chart-wrapper">
<canvas id="barChart"></canvas>
</div>
</div>
<div class="charts-row">
<div class="chart-container">
<h2>Duration Distribution</h2>
<div class="chart-wrapper">
<canvas id="durationChart"></canvas>
</div>
</div>
<div class="chart-container">
<h2>Average Duration by Day</h2>
<div class="chart-wrapper">
<canvas id="avgDurationChart"></canvas>
</div>
</div>
</div>
<div class="list-container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">Recent Sleep Sessions</h2>
<button class="button" onclick="addManualSleep()" style="padding: 8px 16px; font-size: 14px;"> Add Manual Entry</button>
</div>
<div class="sleep-list">
${filteredSleeps
.slice(0, 50)
.map((sleep) => renderSleepItem(sleep))
.join("")}
</div>
</div>
`;
document.getElementById("content").innerHTML = html;
// Render charts
renderBarChart(barChartData);
renderDurationChart(durationDistData);
renderAvgDurationChart(filteredSleeps, timeRange);
}
function calculateDuration(startTime, endTime) {
if (!endTime) return 0;
const start = new Date(startTime);
const end = new Date(endTime);
return Math.round((end - start) / 60000); // minutes
}
function formatDuration(minutes) {
if (minutes === 0) return "0m";
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours === 0) return `${mins}m`;
if (mins === 0) return `${hours}h`;
return `${hours}h ${mins}m`;
}
function prepareBarChartData(sleepSessions, timeRange) {
const now = new Date();
const labels = [];
const durations = []; // Array of arrays, each containing individual session durations
const sessionCounts = []; // For reference
// Only use completed sleep sessions
const completedSessions = sleepSessions.filter((s) => s.end_time);
// If 24 hours selected, show 3-hour aggregates
if (timeRange === 1) {
// Create 8 three-hour buckets
for (let i = 7; i >= 0; i--) {
const periodStart = new Date(now);
periodStart.setHours(periodStart.getHours() - i * 3, 0, 0, 0);
const periodEnd = new Date(periodStart);
periodEnd.setHours(periodEnd.getHours() + 3);
const startLabel = periodStart.toLocaleTimeString("en-US", {
hour: "numeric",
hour12: true,
});
const endLabel = periodEnd.toLocaleTimeString("en-US", {
hour: "numeric",
hour12: true,
});
labels.push(`${startLabel}-${endLabel}`);
// Get individual session durations for this 3-hour period (in hours)
const periodSessions = completedSessions.filter((session) => {
const startDate = new Date(session.start_time);
return startDate >= periodStart && startDate < periodEnd;
});
const sessionDurations = periodSessions.map(
(session) =>
calculateDuration(session.start_time, session.end_time) / 60,
);
durations.push(sessionDurations);
sessionCounts.push(periodSessions.length);
}
} else if (timeRange === 3) {
// Show 8-hour aggregates for 3 days (9 buckets total)
for (let i = 8; i >= 0; i--) {
const periodStart = new Date(now);
periodStart.setHours(periodStart.getHours() - i * 8, 0, 0, 0);
const periodEnd = new Date(periodStart);
periodEnd.setHours(periodEnd.getHours() + 8);
// Format label based on the 8-hour period
const dayStr = periodStart.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
const startHour = periodStart.getHours();
let periodLabel;
if (startHour >= 0 && startHour < 8) {
periodLabel = `${dayStr} Night`;
} else if (startHour >= 8 && startHour < 16) {
periodLabel = `${dayStr} Morning`;
} else {
periodLabel = `${dayStr} Evening`;
}
labels.push(periodLabel);
// Get individual session durations for this 8-hour period (in hours)
const periodSessions = completedSessions.filter((session) => {
const startDate = new Date(session.start_time);
return startDate >= periodStart && startDate < periodEnd;
});
const sessionDurations = periodSessions.map(
(session) =>
calculateDuration(session.start_time, session.end_time) / 60,
);
durations.push(sessionDurations);
sessionCounts.push(periodSessions.length);
}
} else {
// Create days based on timeRange (including today)
for (let i = timeRange - 1; i >= 0; i--) {
const date = new Date(now);
date.setDate(date.getDate() - i);
date.setHours(0, 0, 0, 0);
const dateStr = date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
labels.push(dateStr);
// Get individual session durations for this day (in hours)
const nextDay = new Date(date);
nextDay.setDate(nextDay.getDate() + 1);
const daySessions = completedSessions.filter((session) => {
const startDate = new Date(session.start_time);
return startDate >= date && startDate < nextDay;
});
const sessionDurations = daySessions.map((session) => {
const dur =
calculateDuration(session.start_time, session.end_time) / 60;
console.log(
` Session: ${session.start_time} to ${session.end_time}, duration: ${dur} hours (${calculateDuration(session.start_time, session.end_time)} minutes)`,
);
return dur;
});
console.log(
`${dateStr}: ${daySessions.length} sessions, durations:`,
sessionDurations,
);
durations.push(sessionDurations);
sessionCounts.push(daySessions.length);
}
}
console.log("Final prepareBarChartData result:", {
labels,
durations,
sessionCounts,
});
// Log the actual duration values for debugging
durations.forEach((periodDurations, idx) => {
if (periodDurations.length > 0) {
console.log(` ${labels[idx]}: durations =`, periodDurations);
}
});
return { labels, durations, sessionCounts };
}
function prepareDurationDistributionData(sleeps) {
const ranges = {
"0-30m": 0,
"30m-1h": 0,
"1-2h": 0,
"2-3h": 0,
"3-4h": 0,
"4h+": 0,
};
sleeps.forEach((sleep) => {
const duration = calculateDuration(sleep.start_time, sleep.end_time);
if (duration < 30) ranges["0-30m"]++;
else if (duration < 60) ranges["30m-1h"]++;
else if (duration < 120) ranges["1-2h"]++;
else if (duration < 180) ranges["2-3h"]++;
else if (duration < 240) ranges["3-4h"]++;
else ranges["4h+"]++;
});
return {
labels: Object.keys(ranges),
data: Object.values(ranges),
};
}
let barChartInstance = null;
let durationChartInstance = null;
let avgDurationChartInstance = null;
function renderBarChart(chartData) {
const ctx = document.getElementById("barChart");
// Destroy existing chart if it exists
if (barChartInstance) {
barChartInstance.destroy();
}
// Debug: Log the data
console.log("Bar chart data:", chartData);
// Find the maximum number of sessions in any period
const maxSessions = Math.max(
...chartData.durations.map((d) => d.length),
0,
);
console.log("Max sessions:", maxSessions);
// If no sessions at all, show empty chart
if (maxSessions === 0) {
barChartInstance = new Chart(ctx, {
type: "bar",
data: {
labels: chartData.labels,
datasets: [
{
label: "No Data",
data: new Array(chartData.labels.length).fill(0),
backgroundColor: "rgba(79, 172, 254, 0.3)",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 10,
title: {
display: true,
text: "Hours",
},
},
},
plugins: {
legend: {
display: false,
},
},
},
});
return;
}
// Create color palette for different sessions
const colors = [
"rgba(79, 172, 254, 0.8)",
"rgba(0, 242, 254, 0.8)",
"rgba(58, 155, 232, 0.8)",
"rgba(32, 137, 220, 0.8)",
"rgba(21, 119, 208, 0.8)",
"rgba(11, 101, 196, 0.8)",
"rgba(79, 172, 254, 0.6)",
"rgba(0, 242, 254, 0.6)",
"rgba(58, 155, 232, 0.6)",
"rgba(32, 137, 220, 0.6)",
];
// Create datasets - one for each session position
const datasets = [];
for (let sessionIndex = 0; sessionIndex < maxSessions; sessionIndex++) {
const dataForSession = chartData.durations.map(
(periodDurations) => periodDurations[sessionIndex] || 0,
);
console.log(`Session ${sessionIndex + 1} data:`, dataForSession);
datasets.push({
label: `Session ${sessionIndex + 1}`,
data: dataForSession,
backgroundColor: colors[sessionIndex % colors.length],
borderColor: colors[sessionIndex % colors.length]
.replace("0.8", "1")
.replace("0.6", "1"),
borderWidth: 1,
});
}
console.log("Datasets:", datasets);
// Determine the maximum total duration across all periods (in hours)
const maxDuration = Math.max(
...chartData.durations.map((periodDurations) =>
periodDurations.reduce((sum, val) => sum + val, 0),
),
0,
);
console.log("Max duration (hours):", maxDuration);
// Decide whether to use minutes or hours based on max duration
const useMinutes = maxDuration < 2; // Use minutes if max is less than 2 hours
// Convert data to minutes if needed
const finalDatasets = useMinutes
? datasets.map((dataset) => ({
...dataset,
data: dataset.data.map((hours) => hours * 60), // Convert hours to minutes
}))
: datasets;
barChartInstance = new Chart(ctx, {
type: "bar",
data: {
labels: chartData.labels,
datasets: finalDatasets,
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: useMinutes ? "Minutes" : "Hours",
},
ticks: {
callback: function (value) {
if (useMinutes) {
return Math.round(value) + "m";
} else {
return value.toFixed(1) + "h";
}
},
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
callbacks: {
label: function (context) {
const value = useMinutes
? context.parsed.y / 60
: context.parsed.y;
const hours = Math.floor(value);
const minutes = Math.round((value - hours) * 60);
if (hours === 0) {
return `${context.dataset.label}: ${minutes}m`;
}
return `${context.dataset.label}: ${hours}h ${minutes}m`;
},
footer: function (tooltipItems) {
const periodIndex = tooltipItems[0].dataIndex;
const totalHours = chartData.durations[periodIndex].reduce(
(sum, val) => sum + val,
0,
);
const hours = Math.floor(totalHours);
const minutes = Math.round((totalHours - hours) * 60);
const sessionCount = chartData.sessionCounts[periodIndex];
if (hours === 0) {
return `Total: ${minutes}m (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`;
}
return `Total: ${hours}h ${minutes}m (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`;
},
},
},
},
},
});
}
function renderDurationChart(chartData) {
const ctx = document.getElementById("durationChart");
// Destroy existing chart if it exists
if (durationChartInstance) {
durationChartInstance.destroy();
}
durationChartInstance = new Chart(ctx, {
type: "pie",
data: {
labels: chartData.labels,
datasets: [
{
data: chartData.data,
backgroundColor: [
"rgba(79, 172, 254, 0.8)",
"rgba(0, 242, 254, 0.8)",
"rgba(58, 155, 232, 0.8)",
"rgba(32, 137, 220, 0.8)",
"rgba(21, 119, 208, 0.8)",
"rgba(11, 101, 196, 0.8)",
],
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom",
},
},
},
});
}
function renderAvgDurationChart(sleeps, timeRange) {
// Destroy existing chart if it exists
if (avgDurationChartInstance) {
avgDurationChartInstance.destroy();
}
const days = {};
const today = new Date();
today.setHours(0, 0, 0, 0);
// Initialize all days
for (let i = timeRange - 1; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split("T")[0];
days[dateStr] = { total: 0, count: 0 };
}
// Calculate totals
sleeps
.filter((sleep) => sleep.end_time)
.forEach((sleep) => {
const date = new Date(sleep.start_time);
date.setHours(0, 0, 0, 0);
const dateStr = date.toISOString().split("T")[0];
if (days.hasOwnProperty(dateStr)) {
const duration = calculateDuration(
sleep.start_time,
sleep.end_time,
);
days[dateStr].total += duration;
days[dateStr].count++;
}
});
const labels = Object.keys(days).map((date) => {
const d = new Date(date);
return d.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
});
const data = Object.values(days).map((day) =>
day.count > 0 ? Math.round(day.total / day.count) : 0,
);
const ctx = document.getElementById("avgDurationChart");
avgDurationChartInstance = new Chart(ctx, {
type: "line",
data: {
labels,
datasets: [
{
label: "Avg Duration (minutes)",
data,
borderColor: "rgba(79, 172, 254, 1)",
backgroundColor: "rgba(79, 172, 254, 0.1)",
fill: true,
tension: 0.4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function (value) {
return value + "m";
},
},
},
},
plugins: {
legend: {
display: false,
},
},
},
});
}
function renderSleepItem(sleep) {
const child = allChildren.find((c) => c.id === sleep.child_id);
const childName = child ? child.name : "Unknown";
const startDate = new Date(sleep.start_time);
const isOngoing = !sleep.end_time;
let durationText = "";
if (isOngoing) {
durationText = '<span class="badge ongoing">Ongoing</span>';
} else {
const duration = calculateDuration(sleep.start_time, sleep.end_time);
durationText = `Duration: ${formatDuration(duration)}`;
}
return `
<div class="sleep-item">
<div class="sleep-info">
<div class="sleep-child">${childName}</div>
<div class="sleep-time">
${startDate.toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
})}
${isOngoing ? "" : ` - ${new Date(sleep.end_time).toLocaleString("en-US", { hour: "numeric", minute: "2-digit" })}`}
</div>
<div class="sleep-duration">${durationText}</div>
</div>
<div class="sleep-actions">
<button class="edit-button" onclick="window.location.href='/log-sleep.html?id=${sleep.id}'">Edit</button>
</div>
</div>
`;
}
function addManualSleep() {
window.location.href = `/log-sleep.html`;
}
async function logout() {
try {
await fetch("/api/logout", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
} catch (error) {
console.error("Logout error:", error);
} finally {
localStorage.removeItem("access_token");
localStorage.removeItem("username");
window.location.href = "/login.html";
}
}
// Load data on page load
loadData();
</script>
</body>
</html>