add-feeding-overview #12

Merged
brian merged 3 commits from add-feeding-overview into main 2025-11-10 00:23:10 +01:00
7 changed files with 1208 additions and 32 deletions
-2
View File
@@ -37,8 +37,6 @@ open http://localhost:8000
### Using Docker Compose
```yaml
version: "3.8"
services:
baby-monitor:
image: gitea.gt-proj.com/brian/baby-monitor:latest
+60
View File
@@ -0,0 +1,60 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
# Persist database to local directory
- ./data:/data
environment:
# Set environment to production
- ENVIRONMENT=production
# Set admin credentials
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=password
# Use Redis for token storage
- REDIS_URI=redis://redis:6379/0
# Use PostgreSQL for database
- DATABASE_URL=postgresql://baby_monitor:securepassword@postgres:5432/b
command: ["--host", "0.0.0.0", "--port", "8000"]
restart: unless-stopped
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 1s
postgres:
image: postgres:14-alpine
environment:
- POSTGRES_USER=baby_monitor
- POSTGRES_PASSWORD=securepassword
- POSTGRES_DB=baby_monitor_db
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U baby_monitor -d baby_monitor_db"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
redis-data:
pgdata:
-14
View File
@@ -16,19 +16,5 @@ services:
# Optional: Override admin credentials for development
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=password
# Optional: Use Redis for token storage (uncomment redis service below)
# - REDIS_URI=redis://redis:6379/0
command: ["--host", "0.0.0.0", "--port", "8000", "--reload"]
restart: unless-stopped
# Uncomment to enable Redis token storage
# redis:
# image: redis:7-alpine
# ports:
# - "6379:6379"
# volumes:
# - redis-data:/data
# restart: unless-stopped
# Uncomment if using Redis
# volumes:
# redis-data:
+6
View File
@@ -91,6 +91,12 @@ def serve_log_feeding() -> FileResponse:
return FileResponse(static_path / "log-feeding.html")
@app.get("/feedings.html", include_in_schema=False)
def serve_feedings() -> FileResponse:
"""Serve the feedings overview page."""
return FileResponse(static_path / "feedings.html")
@app.get("/api/")
def read_root(token: Annotated[str, Depends(verify_token)]) -> dict:
"""API root endpoint (requires authentication)."""
File diff suppressed because it is too large Load Diff
+5
View File
@@ -216,6 +216,7 @@
<div class="menu-overlay" id="menuOverlay">
<div class="menu-item" id="menuAddChild">👶 Add Child</div>
<div class="menu-item" id="menuFeedings">🍼 Feedings</div>
<div class="menu-item logout" id="menuLogout">🚪 Logout</div>
</div>
@@ -285,6 +286,10 @@
window.location.href = "/add-child.html";
});
document.getElementById("menuFeedings").addEventListener("click", () => {
window.location.href = "/feedings.html";
});
document.getElementById("menuLogout").addEventListener("click", () => {
closeMenu();
logout();
+79 -16
View File
@@ -212,14 +212,28 @@
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;
// Check if we're editing an existing feeding
const urlParams = new URLSearchParams(window.location.search);
const feedingId = urlParams.get("id");
const isEditMode = !!feedingId;
// Set default start time to now (only for new feedings)
if (!isEditMode) {
const now = new Date();
const localDateTime = new Date(
now.getTime() - now.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 16);
document.getElementById("startTime").value = localDateTime;
}
// Update title and button text if editing
if (isEditMode) {
document.querySelector("h1").textContent = "🍼 Edit Feeding";
document.querySelector(".subtitle").textContent = "Update feeding session details";
submitBtn.textContent = "Update Feeding";
}
// Load user's children
async function loadChildren() {
@@ -255,6 +269,11 @@
// Show form, hide loading
loadingDiv.style.display = "none";
form.style.display = "block";
// If editing, load the feeding data
if (isEditMode) {
loadFeedingData();
}
} else {
showMessage("Failed to load children", "error");
}
@@ -263,6 +282,44 @@
}
}
async function loadFeedingData() {
try {
const response = await fetch(`/api/feedings/${feedingId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const feeding = await response.json();
// Populate form with existing data
document.getElementById("childId").value = feeding.child_id;
// Format datetime for datetime-local input
const startTime = new Date(feeding.start_time);
const startLocalDateTime = new Date(
startTime.getTime() - startTime.getTimezoneOffset() * 60000
).toISOString().slice(0, 16);
document.getElementById("startTime").value = startLocalDateTime;
if (feeding.end_time) {
const endTime = new Date(feeding.end_time);
const endLocalDateTime = new Date(
endTime.getTime() - endTime.getTimezoneOffset() * 60000
).toISOString().slice(0, 16);
document.getElementById("endTime").value = endLocalDateTime;
}
document.getElementById("feedingType").value = feeding.feeding_type;
} else {
showMessage("Failed to load feeding data", "error");
}
} catch (error) {
showMessage("Network error loading feeding", "error");
}
}
loadChildren();
form.addEventListener("submit", async (e) => {
@@ -275,12 +332,15 @@
// Disable button during request
submitBtn.disabled = true;
submitBtn.textContent = "Logging...";
submitBtn.textContent = isEditMode ? "Updating..." : "Logging...";
messageDiv.classList.remove("show");
try {
const response = await fetch("/api/feedings", {
method: "POST",
const url = isEditMode ? `/api/feedings/${feedingId}` : "/api/feedings";
const method = isEditMode ? "PUT" : "POST";
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
@@ -296,14 +356,17 @@
const data = await response.json();
if (response.ok) {
showMessage("Feeding logged successfully!", "success");
// Redirect to home page after 1 second
showMessage(
isEditMode ? "Feeding updated successfully!" : "Feeding logged successfully!",
"success"
);
// Redirect to feedings page after 1 second
setTimeout(() => {
window.location.href = "/";
window.location.href = "/feedings.html";
}, 1000);
} else {
showMessage(
data.detail || "Failed to log feeding. Please try again.",
data.detail || `Failed to ${isEditMode ? 'update' : 'log'} feeding. Please try again.`,
"error",
);
}
@@ -311,7 +374,7 @@
showMessage("Network error. Please try again.", "error");
} finally {
submitBtn.disabled = false;
submitBtn.textContent = "Log Feeding";
submitBtn.textContent = isEditMode ? "Update Feeding" : "Log Feeding";
}
});