Files
baby-monitor/src/baby_monitor/static/index.html
T
Brian Bjarke Jensen 402b5898bb
Build and Push Docker Image / build-and-push (pull_request) Successful in 59s
Python Code Quality / python-code-quality (pull_request) Successful in 10s
Python Test / python-test (pull_request) Successful in 17s
updated admin page with more functionality, simplified burger menu and added many-to-many parent-child relationship
2025-11-10 22:58:52 +01:00

752 lines
22 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor</title>
<link rel="stylesheet" href="/menu.css" />
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
background-color: #f0f0f0;
}
.container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
margin-bottom: 1rem;
}
.user-info {
background: #e3f2fd;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
button {
padding: 0.75rem 1.5rem;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
margin-right: 0.5rem;
}
button:hover {
background-color: #c82333;
}
.admin-button {
background-color: #667eea;
}
.admin-button:hover {
background-color: #5568d3;
}
.hidden {
display: none;
}
.loading {
text-align: center;
padding: 2rem;
}
.feeding-button {
width: 100%;
padding: 2rem;
font-size: 1.5rem;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
transition:
transform 0.2s,
box-shadow 0.2s;
}
.feeding-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.feeding-button.active {
background: linear-gradient(135deg, #f44336 0%, #e91e63 100%);
}
.feeding-button.active:hover {
background: linear-gradient(135deg, #e91e63 0%, #f44336 100%);
}
.feeding-info {
font-size: 0.9rem;
margin-top: 0.5rem;
opacity: 0.9;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1001;
justify-content: center;
align-items: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 400px;
width: 90%;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
.modal h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #333;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #333;
font-weight: 600;
}
.form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.modal-buttons {
display: flex;
gap: 0.5rem;
margin-top: 1.5rem;
}
.modal-buttons button {
flex: 1;
margin: 0;
}
.cancel-button {
background-color: #6c757d;
}
.cancel-button:hover {
background-color: #5a6268;
}
.sleep-button {
width: 100%;
padding: 2rem;
font-size: 1.5rem;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
transition:
transform 0.2s,
box-shadow 0.2s;
}
.sleep-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.sleep-button.active {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.sleep-button.active:hover {
background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%);
}
.sleep-info {
font-size: 0.9rem;
margin-top: 0.5rem;
opacity: 0.9;
}
</style>
</head>
<body>
<div class="container">
<div id="content" class="loading">
<p>Loading...</p>
</div>
</div>
<!-- Start Feeding Modal -->
<div class="modal" id="startFeedingModal">
<div class="modal-content">
<h2>🍼 Start Feeding</h2>
<form id="startFeedingForm">
<div class="form-group">
<label for="modalChildId">Child</label>
<select id="modalChildId" required>
<option value="">Select a child</option>
</select>
</div>
<div class="form-group">
<label for="modalFeedingType">Feeding Type</label>
<select id="modalFeedingType" required>
<option value="">Select type</option>
<option value="left_breast">Left Breast</option>
<option value="right_breast">Right Breast</option>
<option value="bottle">Bottle</option>
</select>
</div>
<div class="modal-buttons">
<button
type="button"
class="cancel-button"
onclick="closeStartFeedingModal()"
>
Cancel
</button>
<button type="submit">Start</button>
</div>
</form>
</div>
</div>
<!-- Start Sleep Modal -->
<div class="modal" id="startSleepModal">
<div class="modal-content">
<h2>😴 Start Sleep</h2>
<form id="startSleepForm">
<div class="form-group">
<label for="modalSleepChildId">Child</label>
<select id="modalSleepChildId" required>
<option value="">Select a child</option>
</select>
</div>
<div class="modal-buttons">
<button
type="button"
class="cancel-button"
onclick="closeStartSleepModal()"
>
Cancel
</button>
<button type="submit">Start</button>
</div>
</form>
</div>
</div>
<script src="/menu.js"></script>
<script>
const token = localStorage.getItem("access_token");
const username = localStorage.getItem("username");
// Check if user is logged in
if (!token) {
window.location.href = "/login.html";
} else {
// Fetch current user info including admin status
fetch("/api/me", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => {
if (response.ok) {
return response.json();
} else {
// Token invalid, redirect to login
localStorage.removeItem("access_token");
localStorage.removeItem("username");
window.location.href = "/login.html";
throw new Error("Authentication failed");
}
})
.then((user) => {
// Initialize burger menu
initBurgerMenu({
includeHome: false,
});
// Check if user has any children
return fetch("/api/children", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => response.json())
.then((children) => {
// If no children, redirect to add-child page
if (children.length === 0) {
window.location.href = "/add-child.html";
return;
}
// Load feeding status and show content
loadFeedingStatus(children);
});
})
.catch((error) => {
console.error("Error:", error);
});
}
let activeFeeding = null;
let userChildren = [];
let lastFeeding = null;
let updateTimerInterval = null;
let activeSleep = null;
let lastSleep = null;
let sleepUpdateTimerInterval = null;
async function loadFeedingStatus(children) {
userChildren = children;
try {
// Fetch active feeding
const activeResponse = await fetch("/api/feedings/active", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (activeResponse.ok) {
activeFeeding = await activeResponse.json();
}
// Fetch all feedings to get the last one
const feedingsResponse = await fetch("/api/feedings", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (feedingsResponse.ok) {
const feedings = await feedingsResponse.json();
if (feedings.length > 0) {
// Feedings are ordered by start_time desc, so first is most recent
lastFeeding = feedings[0];
}
}
// Fetch active sleep
const activeSleepResponse = await fetch("/api/sleep/active", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (activeSleepResponse.ok) {
activeSleep = await activeSleepResponse.json();
}
// Fetch all sleep logs to get the last one
const sleepResponse = await fetch("/api/sleep", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (sleepResponse.ok) {
const sleeps = await sleepResponse.json();
if (sleeps.length > 0) {
lastSleep = sleeps[0];
}
}
} catch (error) {
console.error("Error loading feeding status:", error);
}
renderContent();
startUpdateTimer();
startSleepUpdateTimer();
}
function startUpdateTimer() {
// Clear any existing timer
if (updateTimerInterval) {
clearInterval(updateTimerInterval);
}
// Update every 30 seconds if there's an active feeding
if (activeFeeding) {
updateTimerInterval = setInterval(() => {
if (activeFeeding) {
updateFeedingTimeDisplay();
} else {
clearInterval(updateTimerInterval);
updateTimerInterval = null;
}
}, 30000); // Update every 30 seconds
}
}
function startSleepUpdateTimer() {
// Clear any existing timer
if (sleepUpdateTimerInterval) {
clearInterval(sleepUpdateTimerInterval);
}
// Update every 30 seconds if there's an active sleep
if (activeSleep) {
sleepUpdateTimerInterval = setInterval(() => {
if (activeSleep) {
updateSleepTimeDisplay();
} else {
clearInterval(sleepUpdateTimerInterval);
sleepUpdateTimerInterval = null;
}
}, 30000); // Update every 30 seconds
}
}
function updateFeedingTimeDisplay() {
const timeElement = document.getElementById("feedingTimeInfo");
if (timeElement && activeFeeding) {
timeElement.textContent = formatTime(activeFeeding.start_time);
}
}
function updateSleepTimeDisplay() {
const timeElement = document.getElementById("sleepTimeInfo");
if (timeElement && activeSleep) {
timeElement.textContent = formatTime(activeSleep.start_time);
}
}
function renderContent() {
const feedingButtonHtml = activeFeeding
? `
<button class="feeding-button active" onclick="stopFeeding()">
🛑 Stopped Feeding
<div class="feeding-info">
${activeFeeding.child_name || "Child"} - ${formatFeedingType(activeFeeding.feeding_type)}<br>
Started <span id="feedingTimeInfo">${formatTime(activeFeeding.start_time)}</span>
</div>
</button>
`
: `
<button class="feeding-button" onclick="showStartFeedingModal()">
🍼 Started Feeding
</button>
`;
const sleepButtonHtml = activeSleep
? `
<button class="sleep-button active" onclick="stopSleep()">
🛑 Stopped Sleeping
<div class="sleep-info">
${activeSleep.child_name || "Child"}<br>
Started <span id="sleepTimeInfo">${formatTime(activeSleep.start_time)}</span>
</div>
</button>
`
: `
<button class="sleep-button" onclick="showStartSleepModal()">
😴 Started Sleeping
</button>
`;
document.getElementById("content").innerHTML = `
<h1>Welcome to Baby Monitor!</h1>
<div class="user-info">
<p><strong>Logged in as:</strong> ${username}</p>
</div>
<button class="feeding-button" onclick="window.location.href='/log-diaper.html'" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); margin-bottom: 20px;">
🧷 Changed Diaper
</button>
${feedingButtonHtml}
${sleepButtonHtml}
`;
}
function formatFeedingType(type) {
const types = {
left_breast: "Left Breast",
right_breast: "Right Breast",
bottle: "Bottle",
};
return types[type] || type;
}
function formatTime(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return "just now";
if (diffMins < 60) return `${diffMins} min ago`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ${diffMins % 60}m ago`;
return date.toLocaleString();
}
function showStartFeedingModal() {
const modal = document.getElementById("startFeedingModal");
const childSelect = document.getElementById("modalChildId");
const typeSelect = document.getElementById("modalFeedingType");
// Clear and populate child select
childSelect.innerHTML = '<option value="">Select a child</option>';
userChildren.forEach((child) => {
const option = document.createElement("option");
option.value = child.id;
option.textContent = child.name;
childSelect.appendChild(option);
});
// Set defaults based on last feeding
if (lastFeeding) {
childSelect.value = lastFeeding.child_id;
typeSelect.value = lastFeeding.feeding_type;
}
modal.classList.add("show");
}
function closeStartFeedingModal() {
const modal = document.getElementById("startFeedingModal");
modal.classList.remove("show");
document.getElementById("startFeedingForm").reset();
}
document
.getElementById("startFeedingForm")
.addEventListener("submit", async (e) => {
e.preventDefault();
const childId = parseInt(
document.getElementById("modalChildId").value,
);
const feedingType = document.getElementById("modalFeedingType").value;
try {
const now = new Date();
const localDateTime = new Date(
now.getTime() - now.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 19);
const response = await fetch("/api/feedings", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
child_id: childId,
start_time: localDateTime,
end_time: null,
feeding_type: feedingType,
}),
});
if (response.ok) {
const feeding = await response.json();
activeFeeding = feeding;
lastFeeding = feeding; // Update last feeding
// Add child name to active feeding
const child = userChildren.find((c) => c.id === childId);
if (child) {
activeFeeding.child_name = child.name;
}
closeStartFeedingModal();
renderContent();
startUpdateTimer(); // Start the timer for live updates
} else {
const data = await response.json();
alert(data.detail || "Failed to start feeding");
}
} catch (error) {
console.error("Error starting feeding:", error);
alert("Network error. Please try again.");
}
});
async function stopFeeding() {
if (!activeFeeding) return;
try {
const now = new Date();
const localDateTime = new Date(
now.getTime() - now.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 19);
const response = await fetch(`/api/feedings/${activeFeeding.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
end_time: localDateTime,
}),
});
if (response.ok) {
activeFeeding = null;
if (updateTimerInterval) {
clearInterval(updateTimerInterval);
updateTimerInterval = null;
}
renderContent();
} else {
const data = await response.json();
alert(data.detail || "Failed to stop feeding");
}
} catch (error) {
console.error("Error stopping feeding:", error);
alert("Network error. Please try again.");
}
}
function showStartSleepModal() {
const modal = document.getElementById("startSleepModal");
const childSelect = document.getElementById("modalSleepChildId");
// Clear and populate child select
childSelect.innerHTML = '<option value="">Select a child</option>';
userChildren.forEach((child) => {
const option = document.createElement("option");
option.value = child.id;
option.textContent = child.name;
childSelect.appendChild(option);
});
// Set default based on last sleep
if (lastSleep) {
childSelect.value = lastSleep.child_id;
}
modal.classList.add("show");
}
function closeStartSleepModal() {
const modal = document.getElementById("startSleepModal");
modal.classList.remove("show");
document.getElementById("startSleepForm").reset();
}
document
.getElementById("startSleepForm")
.addEventListener("submit", async (e) => {
e.preventDefault();
const childId = parseInt(
document.getElementById("modalSleepChildId").value,
);
try {
const now = new Date();
const localDateTime = new Date(
now.getTime() - now.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 19);
const response = await fetch("/api/sleep", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
child_id: childId,
start_time: localDateTime,
}),
});
if (response.ok) {
const sleep = await response.json();
activeSleep = sleep;
lastSleep = sleep;
// Add child name to active sleep
const child = userChildren.find((c) => c.id === childId);
if (child) {
activeSleep.child_name = child.name;
}
closeStartSleepModal();
renderContent();
startSleepUpdateTimer();
} else {
const data = await response.json();
alert(data.detail || "Failed to start sleep");
}
} catch (error) {
console.error("Error starting sleep:", error);
alert("Network error. Please try again.");
}
});
async function stopSleep() {
if (!activeSleep) return;
try {
const now = new Date();
const localDateTime = new Date(
now.getTime() - now.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 19);
const response = await fetch(`/api/sleep/${activeSleep.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
end_time: localDateTime,
}),
});
if (response.ok) {
activeSleep = null;
if (sleepUpdateTimerInterval) {
clearInterval(sleepUpdateTimerInterval);
sleepUpdateTimerInterval = null;
}
renderContent();
} else {
const data = await response.json();
alert(data.detail || "Failed to stop sleep");
}
} catch (error) {
console.error("Error stopping sleep:", error);
alert("Network error. Please try again.");
}
}
async function logout() {
const token = localStorage.getItem("access_token");
try {
await fetch("/api/logout", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
} catch (error) {
console.error("Logout error:", error);
} finally {
// Clear local storage and redirect
localStorage.removeItem("access_token");
localStorage.removeItem("username");
window.location.href = "/login.html";
}
}
</script>
</body>
</html>