added feeding data objects and workflow
Build and Push Docker Image / build-and-push (pull_request) Successful in 57s
Python Code Quality / python-code-quality (pull_request) Successful in 10s
Python Test / python-test (pull_request) Successful in 17s

This commit is contained in:
Brian Bjarke Jensen
2025-11-09 17:22:37 +01:00
parent 1f48083db8
commit 55de11dcdd
11 changed files with 1104 additions and 8 deletions
+365 -8
View File
@@ -117,6 +117,92 @@
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;
}
</style>
</head>
<body>
@@ -139,6 +225,40 @@
</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>
<script>
const token = localStorage.getItem("access_token");
const username = localStorage.getItem("username");
@@ -206,14 +326,8 @@
return;
}
// Show authenticated content
document.getElementById("content").innerHTML = `
<h1>Welcome to Baby Monitor!</h1>
<div class="user-info">
<p><strong>Logged in as:</strong> ${username}</p>
</div>
<p>Your session is active.</p>
`;
// Load feeding status and show content
loadFeedingStatus(children);
});
})
.catch((error) => {
@@ -221,6 +335,249 @@
});
}
let activeFeeding = null;
let userChildren = [];
let lastFeeding = null;
let updateTimerInterval = 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];
}
}
} catch (error) {
console.error("Error loading feeding status:", error);
}
renderContent();
startUpdateTimer();
}
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 updateFeedingTimeDisplay() {
const timeElement = document.getElementById("feedingTimeInfo");
if (timeElement && activeFeeding) {
timeElement.textContent = formatTime(activeFeeding.start_time);
}
}
function renderContent() {
const feedingButtonHtml = activeFeeding
? `
<button class="feeding-button active" onclick="stopFeeding()">
🛑 Stop 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()">
▶️ Start Feeding
</button>
`;
document.getElementById("content").innerHTML = `
<h1>Welcome to Baby Monitor!</h1>
<div class="user-info">
<p><strong>Logged in as:</strong> ${username}</p>
</div>
${feedingButtonHtml}
<p>Your session is active.</p>
`;
}
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.");
}
}
async function logout() {
const token = localStorage.getItem("access_token");
+328
View File
@@ -0,0 +1,328 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Log Feeding - Baby Monitor</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
max-width: 500px;
width: 100%;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
font-size: 14px;
}
input,
select {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
input:focus,
select:focus {
outline: none;
border-color: #667eea;
}
.button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 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;
width: 100%;
margin-top: 10px;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.button:active {
transform: translateY(0);
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.button.secondary {
background: #6c757d;
margin-top: 10px;
}
.button.secondary:hover {
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.4);
}
.message {
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
display: none;
}
.message.show {
display: block;
}
.message.error {
background: #ffebee;
border-left: 4px solid #f44336;
color: #c62828;
}
.message.success {
background: #e8f5e9;
border-left: 4px solid #4caf50;
color: #2e7d32;
}
.help-text {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>🍼 Log Feeding</h1>
<p class="subtitle">Record a feeding session</p>
<div id="message" class="message"></div>
<div id="loadingChildren" class="loading">Loading children...</div>
<form id="logFeedingForm" style="display: none">
<div class="form-group">
<label for="childId">Child *</label>
<select id="childId" name="childId" required>
<option value="">Select a child</option>
</select>
</div>
<div class="form-group">
<label for="startTime">Start Time *</label>
<input
type="datetime-local"
id="startTime"
name="startTime"
required
/>
<p class="help-text">Defaults to current time</p>
</div>
<div class="form-group">
<label for="endTime">End Time</label>
<input type="datetime-local" id="endTime" name="endTime" />
<p class="help-text">Leave empty if feeding is ongoing</p>
</div>
<div class="form-group">
<label for="feedingType">Feeding Type *</label>
<select id="feedingType" name="feedingType" 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>
<button type="submit" class="button" id="submitBtn">Log Feeding</button>
<button type="button" class="button secondary" onclick="goBack()">
Cancel
</button>
</form>
</div>
<script>
// Check if user is authenticated
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
const form = document.getElementById("logFeedingForm");
const messageDiv = document.getElementById("message");
const submitBtn = document.getElementById("submitBtn");
const loadingDiv = document.getElementById("loadingChildren");
const childSelect = document.getElementById("childId");
// Set default start time to now
const now = new Date();
const localDateTime = new Date(
now.getTime() - now.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 16);
document.getElementById("startTime").value = localDateTime;
// Load user's children
async function loadChildren() {
try {
const response = await fetch("/api/children", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const children = await response.json();
if (children.length === 0) {
showMessage(
"No children found. Please add a child first.",
"error",
);
setTimeout(() => {
window.location.href = "/add-child.html";
}, 2000);
return;
}
// Populate select dropdown
children.forEach((child) => {
const option = document.createElement("option");
option.value = child.id;
option.textContent = child.name;
childSelect.appendChild(option);
});
// Show form, hide loading
loadingDiv.style.display = "none";
form.style.display = "block";
} else {
showMessage("Failed to load children", "error");
}
} catch (error) {
showMessage("Network error loading children", "error");
}
}
loadChildren();
form.addEventListener("submit", async (e) => {
e.preventDefault();
const childId = parseInt(document.getElementById("childId").value);
const startTime = document.getElementById("startTime").value;
const endTimeValue = document.getElementById("endTime").value;
const feedingType = document.getElementById("feedingType").value;
// Disable button during request
submitBtn.disabled = true;
submitBtn.textContent = "Logging...";
messageDiv.classList.remove("show");
try {
const response = await fetch("/api/feedings", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
child_id: childId,
start_time: startTime,
end_time: endTimeValue || null,
feeding_type: feedingType,
}),
});
const data = await response.json();
if (response.ok) {
showMessage("Feeding logged successfully!", "success");
// Redirect to home page after 1 second
setTimeout(() => {
window.location.href = "/";
}, 1000);
} else {
showMessage(
data.detail || "Failed to log feeding. Please try again.",
"error",
);
}
} catch (error) {
showMessage("Network error. Please try again.", "error");
} finally {
submitBtn.disabled = false;
submitBtn.textContent = "Log Feeding";
}
});
function showMessage(text, type) {
messageDiv.textContent = text;
messageDiv.className = `message ${type} show`;
}
function goBack() {
window.location.href = "/";
}
</script>
</body>
</html>